pr_27257.patch 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469
  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 0ccadaf5a95..4111c42c4d1 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. @@ -111,10 +111,8 @@ function parseArgs() {
  6. if (['true', 'false'].includes(v)) return v === 'true'
  7. throw new Error(`expected "true" or "false" for boolean option ${name}`)
  8. }
  9. - const BATCH_RANGE_START = objectIdFromInput(
  10. - args['BATCH_RANGE_START']
  11. - ).toString()
  12. - const BATCH_RANGE_END = objectIdFromInput(args['BATCH_RANGE_END']).toString()
  13. + const BATCH_RANGE_START = objectIdFromInput(args.BATCH_RANGE_START).toString()
  14. + const BATCH_RANGE_END = objectIdFromInput(args.BATCH_RANGE_END).toString()
  15. return {
  16. PROCESS_NON_DELETED_PROJECTS: boolVal('processNonDeletedProjects'),
  17. PROCESS_DELETED_PROJECTS: boolVal('processDeletedProjects'),
  18. @@ -122,8 +120,8 @@ function parseArgs() {
  19. PROCESS_HASHED_FILES: boolVal('processHashedFiles'),
  20. BATCH_RANGE_START,
  21. BATCH_RANGE_END,
  22. - LOGGING_IDENTIFIER: args['LOGGING_IDENTIFIER'] || BATCH_RANGE_START,
  23. - PROJECT_IDS_FROM: args['projectIdsFrom'],
  24. + LOGGING_IDENTIFIER: args.LOGGING_IDENTIFIER || BATCH_RANGE_START,
  25. + PROJECT_IDS_FROM: args.projectIdsFrom,
  26. }
  27. }
  28. @@ -249,8 +247,8 @@ let lastEventLoopStats = performance.eventLoopUtilization()
  29. * @param {number} ms
  30. */
  31. function toMiBPerSecond(v, ms) {
  32. - const ONE_MiB = 1024 * 1024
  33. - return v / ONE_MiB / (ms / 1000)
  34. + const MiB = 1024 * 1024
  35. + return v / MiB / (ms / 1000)
  36. }
  37. /**
  38. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  39. index 4111c42c4d1..2d55b41b43e 100644
  40. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  41. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  42. @@ -84,11 +84,11 @@ ObjectId.cacheHexString = true
  43. function parseArgs() {
  44. const PUBLIC_LAUNCH_DATE = new Date('2012-01-01T00:00:00Z')
  45. const args = commandLineArgs([
  46. - { name: 'processNonDeletedProjects', type: String, defaultValue: 'false' },
  47. - { name: 'processDeletedProjects', type: String, defaultValue: 'false' },
  48. - { name: 'processHashedFiles', type: String, defaultValue: 'false' },
  49. - { name: 'processBlobs', type: String, defaultValue: 'true' },
  50. - { name: 'projectIdsFrom', type: String, defaultValue: '' },
  51. + { name: 'projects', type: Boolean },
  52. + { name: 'deleted-projects', type: Boolean },
  53. + { name: 'include-hashed-files', type: Boolean },
  54. + { name: 'skip-existing-blobs', type: Boolean },
  55. + { name: 'from-file', type: String, defaultValue: '' },
  56. {
  57. name: 'BATCH_RANGE_START',
  58. type: String,
  59. @@ -99,29 +99,20 @@ function parseArgs() {
  60. type: String,
  61. defaultValue: new Date().toISOString(),
  62. },
  63. - { name: 'LOGGING_IDENTIFIER', type: String, defaultValue: '' },
  64. + { name: 'logging-id', type: String, defaultValue: '' },
  65. ])
  66. - /**
  67. - * commandLineArgs cannot handle --foo=false, so go the long way
  68. - * @param {string} name
  69. - * @return {boolean}
  70. - */
  71. - function boolVal(name) {
  72. - const v = args[name]
  73. - if (['true', 'false'].includes(v)) return v === 'true'
  74. - throw new Error(`expected "true" or "false" for boolean option ${name}`)
  75. - }
  76. +
  77. const BATCH_RANGE_START = objectIdFromInput(args.BATCH_RANGE_START).toString()
  78. const BATCH_RANGE_END = objectIdFromInput(args.BATCH_RANGE_END).toString()
  79. return {
  80. - PROCESS_NON_DELETED_PROJECTS: boolVal('processNonDeletedProjects'),
  81. - PROCESS_DELETED_PROJECTS: boolVal('processDeletedProjects'),
  82. - PROCESS_BLOBS: boolVal('processBlobs'),
  83. - PROCESS_HASHED_FILES: boolVal('processHashedFiles'),
  84. + PROCESS_NON_DELETED_PROJECTS: args.projects,
  85. + PROCESS_DELETED_PROJECTS: args['deleted-projects'],
  86. + PROCESS_HASHED_FILES: args['include-hashed-files'],
  87. + PROCESS_BLOBS: !args['skip-existing-blobs'],
  88. BATCH_RANGE_START,
  89. BATCH_RANGE_END,
  90. - LOGGING_IDENTIFIER: args.LOGGING_IDENTIFIER || BATCH_RANGE_START,
  91. - PROJECT_IDS_FROM: args.projectIdsFrom,
  92. + LOGGING_IDENTIFIER: args['logging-id'] || BATCH_RANGE_START,
  93. + PROJECT_IDS_FROM: args['from-file'],
  94. }
  95. }
  96. 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
  97. index 62b0b1de25f..0f8bdbf3e1a 100644
  98. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  99. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  100. @@ -544,8 +544,8 @@ describe('back_fill_file_hash script', function () {
  101. process.argv0,
  102. [
  103. 'storage/scripts/back_fill_file_hash.mjs',
  104. - '--processNonDeletedProjects=true',
  105. - '--processDeletedProjects=true',
  106. + '--projects',
  107. + '--deleted-projects',
  108. ...args,
  109. ],
  110. {
  111. @@ -854,7 +854,7 @@ describe('back_fill_file_hash script', function () {
  112. // Practically, this is slow and moving it to the end of the tests gets us there most of the way.
  113. it('should process nothing on re-run', async function () {
  114. const rerun = await runScript(
  115. - processHashedFiles ? ['--processHashedFiles=true'] : [],
  116. + processHashedFiles ? ['--include-hashed-files'] : [],
  117. {},
  118. false
  119. )
  120. @@ -1113,7 +1113,7 @@ describe('back_fill_file_hash script', function () {
  121. output1 = await runScript([], {})
  122. })
  123. before('run script with hashed files', async function () {
  124. - output2 = await runScript(['--processHashedFiles=true'], {})
  125. + output2 = await runScript(['--include-hashed-files'], {})
  126. })
  127. it('should print stats for the first run without hashed files', function () {
  128. expect(output1.stats).deep.equal(STATS_ALL)
  129. @@ -1161,7 +1161,7 @@ describe('back_fill_file_hash script', function () {
  130. let output
  131. before('prepare environment', prepareEnvironment)
  132. before('run script', async function () {
  133. - output = await runScript(['--processHashedFiles=true'], {})
  134. + output = await runScript(['--include-hashed-files'], {})
  135. })
  136. it('should print stats', function () {
  137. expect(output.stats).deep.equal(
  138. @@ -1263,10 +1263,10 @@ describe('back_fill_file_hash script', function () {
  139. let outputPart0, outputPart1
  140. before('run script on part 0', async function () {
  141. - outputPart0 = await runScript([`--projectIdsFrom=${path0}`])
  142. + outputPart0 = await runScript([`--from-file=${path0}`])
  143. })
  144. before('run script on part 1', async function () {
  145. - outputPart1 = await runScript([`--projectIdsFrom=${path1}`])
  146. + outputPart1 = await runScript([`--from-file=${path1}`])
  147. })
  148. /**
  149. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  150. index 2d55b41b43e..68ce4b67aa2 100644
  151. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  152. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  153. @@ -79,7 +79,7 @@ ObjectId.cacheHexString = true
  154. */
  155. /**
  156. - * @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}}
  157. + * @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, DRY_RUN: boolean}}
  158. */
  159. function parseArgs() {
  160. const PUBLIC_LAUNCH_DATE = new Date('2012-01-01T00:00:00Z')
  161. @@ -89,6 +89,7 @@ function parseArgs() {
  162. { name: 'include-hashed-files', type: Boolean },
  163. { name: 'skip-existing-blobs', type: Boolean },
  164. { name: 'from-file', type: String, defaultValue: '' },
  165. + { name: 'dry-run', type: Boolean },
  166. {
  167. name: 'BATCH_RANGE_START',
  168. type: String,
  169. @@ -109,6 +110,7 @@ function parseArgs() {
  170. PROCESS_DELETED_PROJECTS: args['deleted-projects'],
  171. PROCESS_HASHED_FILES: args['include-hashed-files'],
  172. PROCESS_BLOBS: !args['skip-existing-blobs'],
  173. + DRY_RUN: args['dry-run'],
  174. BATCH_RANGE_START,
  175. BATCH_RANGE_END,
  176. LOGGING_IDENTIFIER: args['logging-id'] || BATCH_RANGE_START,
  177. @@ -121,6 +123,7 @@ const {
  178. PROCESS_DELETED_PROJECTS,
  179. PROCESS_BLOBS,
  180. PROCESS_HASHED_FILES,
  181. + DRY_RUN,
  182. BATCH_RANGE_START,
  183. BATCH_RANGE_END,
  184. LOGGING_IDENTIFIER,
  185. @@ -325,10 +328,12 @@ async function processFileWithCleanup(entry) {
  186. try {
  187. return await processFile(entry, filePath)
  188. } finally {
  189. - await Promise.all([
  190. - fs.promises.rm(filePath, { force: true }),
  191. - fs.promises.rm(filePath + GZ_SUFFIX, { force: true }),
  192. - ])
  193. + if (!DRY_RUN) {
  194. + await Promise.all([
  195. + fs.promises.rm(filePath, { force: true }),
  196. + fs.promises.rm(filePath + GZ_SUFFIX, { force: true }),
  197. + ])
  198. + }
  199. }
  200. }
  201. @@ -383,6 +388,12 @@ async function processFileOnce(entry, filePath) {
  202. // know the hash of.
  203. return entry.hash
  204. }
  205. + if (DRY_RUN) {
  206. + console.log(
  207. + `DRY-RUN: would process file ${fileId} for project ${projectId}`
  208. + )
  209. + return 'dry-run'
  210. + }
  211. const blobStore = new BlobStore(historyId)
  212. STATS.readFromGCSCount++
  213. // make a fetch request to filestore itself
  214. diff --git a/libraries/logger/logging-manager.js b/libraries/logger/logging-manager.js
  215. index edf922be72b..9fb4f284053 100644
  216. --- a/libraries/logger/logging-manager.js
  217. +++ b/libraries/logger/logging-manager.js
  218. @@ -11,7 +11,7 @@ const LoggingManager = {
  219. /**
  220. * @param {string} name - The name of the logger
  221. */
  222. - initialize(name) {
  223. + initialize(name, options = {}) {
  224. this.isProduction =
  225. (process.env.NODE_ENV || '').toLowerCase() === 'production'
  226. const isTest = (process.env.NODE_ENV || '').toLowerCase() === 'test'
  227. @@ -27,7 +27,7 @@ const LoggingManager = {
  228. req: Serializers.req,
  229. res: Serializers.res,
  230. },
  231. - streams: [this._getOutputStreamConfig()],
  232. + streams: options.streams ?? [this._getOutputStreamConfig()],
  233. })
  234. this._setupRingBuffer()
  235. this._setupLogLevelChecker()
  236. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  237. index 68ce4b67aa2..a7f220ec362 100644
  238. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  239. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  240. @@ -79,10 +79,14 @@ ObjectId.cacheHexString = true
  241. */
  242. /**
  243. - * @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, DRY_RUN: boolean}}
  244. + * @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, DRY_RUN: boolean, OUTPUT_FILE: string, PROCESS_BLOBS: boolean}}
  245. */
  246. function parseArgs() {
  247. const PUBLIC_LAUNCH_DATE = new Date('2012-01-01T00:00:00Z')
  248. + const DEFAULT_OUTPUT_FILE = `file-migration-${new Date()
  249. + .toISOString()
  250. + .replace(/[:.]/g, '_')}.log`
  251. +
  252. const args = commandLineArgs([
  253. { name: 'projects', type: Boolean },
  254. { name: 'deleted-projects', type: Boolean },
  255. @@ -90,6 +94,7 @@ function parseArgs() {
  256. { name: 'skip-existing-blobs', type: Boolean },
  257. { name: 'from-file', type: String, defaultValue: '' },
  258. { name: 'dry-run', type: Boolean },
  259. + { name: 'output', type: String, defaultValue: DEFAULT_OUTPUT_FILE },
  260. {
  261. name: 'BATCH_RANGE_START',
  262. type: String,
  263. @@ -111,6 +116,7 @@ function parseArgs() {
  264. PROCESS_HASHED_FILES: args['include-hashed-files'],
  265. PROCESS_BLOBS: !args['skip-existing-blobs'],
  266. DRY_RUN: args['dry-run'],
  267. + OUTPUT_FILE: args.output,
  268. BATCH_RANGE_START,
  269. BATCH_RANGE_END,
  270. LOGGING_IDENTIFIER: args['logging-id'] || BATCH_RANGE_START,
  271. @@ -124,6 +130,7 @@ const {
  272. PROCESS_BLOBS,
  273. PROCESS_HASHED_FILES,
  274. DRY_RUN,
  275. + OUTPUT_FILE,
  276. BATCH_RANGE_START,
  277. BATCH_RANGE_END,
  278. LOGGING_IDENTIFIER,
  279. @@ -158,6 +165,21 @@ const STREAM_HIGH_WATER_MARK = parseInt(
  280. const LOGGING_INTERVAL = parseInt(process.env.LOGGING_INTERVAL || '60000', 10)
  281. const SLEEP_BEFORE_EXIT = parseInt(process.env.SLEEP_BEFORE_EXIT || '1000', 10)
  282. +// Log output to a file
  283. +logger.initialize('file-migration', {
  284. + streams: [
  285. + {
  286. + stream:
  287. + OUTPUT_FILE === '-'
  288. + ? process.stdout
  289. + : fs.createWriteStream(OUTPUT_FILE, { flags: 'a' }),
  290. + },
  291. + ],
  292. +})
  293. +async function trackProgress(progress) {
  294. + logger.info({}, progress)
  295. +}
  296. +
  297. // Filestore endpoint location
  298. const FILESTORE_HOST = process.env.FILESTORE_HOST || '127.0.0.1'
  299. const FILESTORE_PORT = process.env.FILESTORE_PORT || '3009'
  300. @@ -525,8 +547,9 @@ async function queueNextBatch(batch, prefix = 'rootFolder.0') {
  301. const end = renderObjectId(batch[batch.length - 1]._id)
  302. const deferred = processBatch(batch, prefix)
  303. .then(() => {
  304. - console.error(`Actually completed batch ending ${end}`)
  305. + logger.info({ end }, 'actually completed batch')
  306. })
  307. +
  308. .catch(err => {
  309. logger.error({ err, start, end }, 'fatal error processing batch')
  310. throw err
  311. @@ -1062,6 +1085,7 @@ async function processNonDeletedProjects() {
  312. {
  313. BATCH_RANGE_START,
  314. BATCH_RANGE_END,
  315. + trackProgress,
  316. }
  317. )
  318. } catch (err) {
  319. 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
  320. index 0f8bdbf3e1a..117352d6164 100644
  321. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  322. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  323. @@ -544,6 +544,7 @@ describe('back_fill_file_hash script', function () {
  324. process.argv0,
  325. [
  326. 'storage/scripts/back_fill_file_hash.mjs',
  327. + '--output=-',
  328. '--projects',
  329. '--deleted-projects',
  330. ...args,
  331. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  332. index a7f220ec362..4beba19cf4c 100644
  333. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  334. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  335. @@ -88,6 +88,7 @@ function parseArgs() {
  336. .replace(/[:.]/g, '_')}.log`
  337. const args = commandLineArgs([
  338. + { name: 'all', alias: 'a', type: Boolean },
  339. { name: 'projects', type: Boolean },
  340. { name: 'deleted-projects', type: Boolean },
  341. { name: 'include-hashed-files', type: Boolean },
  342. @@ -108,6 +109,36 @@ function parseArgs() {
  343. { name: 'logging-id', type: String, defaultValue: '' },
  344. ])
  345. + // If no arguments are provided, display a usage message
  346. + if (process.argv.length <= 2) {
  347. + console.error(
  348. + 'Usage: node back_fill_file_hash.mjs --all | --projects | --deleted-projects'
  349. + )
  350. + process.exit(1)
  351. + }
  352. +
  353. + // Require at least one of --projects, --deleted-projects and --all
  354. + if (!args.projects && !args['deleted-projects'] && !args.all) {
  355. + console.error(
  356. + 'Must specify at least one of --projects and --deleted-projects, or --all'
  357. + )
  358. + process.exit(1)
  359. + }
  360. +
  361. + // Forbid --all with --projects or --deleted-projects
  362. + if (args.all && (args.projects || args['deleted-projects'])) {
  363. + console.error('Cannot use --all with --projects or --deleted-projects')
  364. + process.exit(1)
  365. + }
  366. +
  367. + // The --all option processes all projects, including deleted ones
  368. + // and checks existing hashed files are present in the blob store.
  369. + if (args.all) {
  370. + args.projects = true
  371. + args['deleted-projects'] = true
  372. + args['include-hashed-files'] = true
  373. + }
  374. +
  375. const BATCH_RANGE_START = objectIdFromInput(args.BATCH_RANGE_START).toString()
  376. const BATCH_RANGE_END = objectIdFromInput(args.BATCH_RANGE_END).toString()
  377. return {
  378. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  379. index 4beba19cf4c..492c5ad939d 100644
  380. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  381. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  382. @@ -88,6 +88,7 @@ function parseArgs() {
  383. .replace(/[:.]/g, '_')}.log`
  384. const args = commandLineArgs([
  385. + { name: 'help', alias: 'h', type: Boolean },
  386. { name: 'all', alias: 'a', type: Boolean },
  387. { name: 'projects', type: Boolean },
  388. { name: 'deleted-projects', type: Boolean },
  389. @@ -117,6 +118,48 @@ function parseArgs() {
  390. process.exit(1)
  391. }
  392. + // If --help is provided, display the help message
  393. + if (args.help) {
  394. + console.log(`Usage: node back_fill_file_hash.mjs [options]
  395. +
  396. +Project selection options:
  397. + --all, -a Process all projects, including deleted ones
  398. + --projects Process projects (excluding deleted ones)
  399. + --deleted-projects Process deleted projects
  400. + --from-file <file> Process selected projects ids from file
  401. +
  402. +File selection options:
  403. + --include-hashed-files Process files that already have a hash
  404. + --skip-existing-blobs Skip processing files already in the blob store
  405. +
  406. +Logging options:
  407. + --output <file> Output log to the specified file
  408. + (default: file-migration-<timestamp>.log)
  409. + --logging-id <id> Identifier for logging
  410. + (default: BATCH_RANGE_START)
  411. +
  412. +Batch range options:
  413. + --BATCH_RANGE_START <date> Start date for processing
  414. + (default: ${args.BATCH_RANGE_START})
  415. + --BATCH_RANGE_END <date> End date for processing
  416. + (default: ${args.BATCH_RANGE_END})
  417. +
  418. +Other options:
  419. + --dry-run Perform a dry run without making changes
  420. + --help, -h Show this help message
  421. +
  422. +Typical usage:
  423. +
  424. + node back_fill_file_hash.mjs --all
  425. +
  426. +is equivalent to
  427. +
  428. + node back_fill_file_hash.mjs --projects --deleted-projects \\
  429. + --include-hashed-files
  430. +`)
  431. + process.exit(0)
  432. + }
  433. +
  434. // Require at least one of --projects, --deleted-projects and --all
  435. if (!args.projects && !args['deleted-projects'] && !args.all) {
  436. console.error(
  437. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  438. index 492c5ad939d..b20e365c4ff 100644
  439. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  440. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  441. @@ -79,7 +79,7 @@ ObjectId.cacheHexString = true
  442. */
  443. /**
  444. - * @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, DRY_RUN: boolean, OUTPUT_FILE: string, PROCESS_BLOBS: boolean}}
  445. + * @return {{PROJECT_IDS_FROM: string, PROCESS_HASHED_FILES: boolean, LOGGING_IDENTIFIER: string, BATCH_RANGE_START: string, BATCH_RANGE_END: string, PROCESS_NON_DELETED_PROJECTS: boolean, PROCESS_DELETED_PROJECTS: boolean, PROCESS_BLOBS: boolean, DRY_RUN: boolean, OUTPUT_FILE: string, DISPLAY_REPORT: boolean}}
  446. */
  447. function parseArgs() {
  448. const PUBLIC_LAUNCH_DATE = new Date('2012-01-01T00:00:00Z')
  449. @@ -97,6 +97,7 @@ function parseArgs() {
  450. { name: 'from-file', type: String, defaultValue: '' },
  451. { name: 'dry-run', type: Boolean },
  452. { name: 'output', type: String, defaultValue: DEFAULT_OUTPUT_FILE },
  453. + { name: 'report', type: Boolean },
  454. {
  455. name: 'BATCH_RANGE_START',
  456. type: String,
  457. @@ -145,6 +146,7 @@ Batch range options:
  458. (default: ${args.BATCH_RANGE_END})
  459. Other options:
  460. + --report Display a report of the current status
  461. --dry-run Perform a dry run without making changes
  462. --help, -h Show this help message
  463. @@ -160,10 +162,15 @@ is equivalent to
  464. process.exit(0)
  465. }
  466. - // Require at least one of --projects, --deleted-projects and --all
  467. - if (!args.projects && !args['deleted-projects'] && !args.all) {
  468. + // Require at least one of --projects, --deleted-projects and --all or --report
  469. + if (
  470. + !args.projects &&
  471. + !args['deleted-projects'] &&
  472. + !args.all &&
  473. + !args.report
  474. + ) {
  475. console.error(
  476. - 'Must specify at least one of --projects and --deleted-projects, or --all'
  477. + 'Must specify at least one of --projects and --deleted-projects, --all or --report'
  478. )
  479. process.exit(1)
  480. }
  481. @@ -174,6 +181,14 @@ is equivalent to
  482. process.exit(1)
  483. }
  484. + // Forbid --all, --projects, --deleted-projects with --report
  485. + if (args.report && (args.all || args.projects || args['deleted-projects'])) {
  486. + console.error(
  487. + 'Cannot use --report with --all, --projects or --deleted-projects'
  488. + )
  489. + process.exit(1)
  490. + }
  491. +
  492. // The --all option processes all projects, including deleted ones
  493. // and checks existing hashed files are present in the blob store.
  494. if (args.all) {
  495. @@ -195,6 +210,7 @@ is equivalent to
  496. BATCH_RANGE_END,
  497. LOGGING_IDENTIFIER: args['logging-id'] || BATCH_RANGE_START,
  498. PROJECT_IDS_FROM: args['from-file'],
  499. + DISPLAY_REPORT: args.report,
  500. }
  501. }
  502. @@ -209,6 +225,7 @@ const {
  503. BATCH_RANGE_END,
  504. LOGGING_IDENTIFIER,
  505. PROJECT_IDS_FROM,
  506. + DISPLAY_REPORT,
  507. } = parseArgs()
  508. // We need to handle the start and end differently as ids of deleted projects are created at time of deletion.
  509. @@ -254,6 +271,108 @@ async function trackProgress(progress) {
  510. logger.info({}, progress)
  511. }
  512. +/**
  513. + * Display the stats for the projects or deletedProjects collections.
  514. + *
  515. + * @param {number} N - Number of samples to take from the collection.
  516. + * @param {string} name - Name of the collection being sampled.
  517. + * @param {Collection} collection - MongoDB collection to query.
  518. + * @param {Object} query - MongoDB query to filter documents.
  519. + * @param {Object} projection - MongoDB projection to select fields.
  520. + * @param {number} collectionCount - Total number of documents in the collection.
  521. + * @returns {Promise<void>} Resolves when stats have been displayed.
  522. + */
  523. +async function getStatsForCollection(
  524. + N,
  525. + name,
  526. + collection,
  527. + query,
  528. + projection,
  529. + collectionCount
  530. +) {
  531. + const stats = {
  532. + projectCount: 0,
  533. + projectsWithAllHashes: 0,
  534. + fileCount: 0,
  535. + fileWithHashCount: 0,
  536. + }
  537. + // Pick a random sample of projects and estimate the number of files without hashes
  538. + const result = await collection
  539. + .aggregate([
  540. + { $sample: { size: N } },
  541. + { $match: query },
  542. + {
  543. + $project: projection,
  544. + },
  545. + ])
  546. + .toArray()
  547. +
  548. + for (const project of result) {
  549. + const fileTree = JSON.stringify(project, [
  550. + 'rootFolder',
  551. + 'folders',
  552. + 'fileRefs',
  553. + 'hash',
  554. + '_id',
  555. + ])
  556. + // count the number of files without a hash, these are uniquely identified
  557. + // by entries with {"_id":"...."} since we have filtered the file tree
  558. + const filesWithoutHash = fileTree.match(/\{"_id":"[0-9a-f]{24}"\}/g) || []
  559. + // count the number of files with a hash, these are uniquely identified
  560. + // by the number of "hash" strings due to the filtering
  561. + const filesWithHash = fileTree.match(/"hash"/g) || []
  562. + stats.fileCount += filesWithoutHash.length + filesWithHash.length
  563. + stats.fileWithHashCount += filesWithHash.length
  564. + stats.projectCount++
  565. + stats.projectsWithAllHashes += filesWithoutHash.length === 0 ? 1 : 0
  566. + }
  567. + console.log(`Sampled stats for ${name}:`)
  568. + const fractionSampled = stats.projectCount / collectionCount
  569. + const percentageSampled = (fractionSampled * 100).toFixed(1)
  570. + const fractionConverted = stats.projectsWithAllHashes / stats.projectCount
  571. + const percentageConverted = (fractionConverted * 100).toFixed(1)
  572. + console.log(
  573. + `- Sampled ${name}: ${stats.projectCount} (${percentageSampled}%)`
  574. + )
  575. + console.log(
  576. + `- Sampled ${name} with all hashes present: ${stats.projectsWithAllHashes}`
  577. + )
  578. + console.log(
  579. + `- Percentage of ${name} converted: ${percentageConverted}% (estimated)`
  580. + )
  581. +}
  582. +
  583. +/**
  584. + * Displays a report of the current status of projects and deleted projects,
  585. + * including counts and estimated progress based on a sample.
  586. + */
  587. +async function displayReport() {
  588. + const projectsCountResult = await projectsCollection.countDocuments()
  589. + const deletedProjectsCountResult =
  590. + await deletedProjectsCollection.countDocuments()
  591. + const sampleSize = 1000
  592. + console.log('Current status:')
  593. + console.log(`- Projects: ${projectsCountResult}`)
  594. + console.log(`- Deleted projects: ${deletedProjectsCountResult}`)
  595. + console.log(`Sampling ${sampleSize} projects to estimate progress...`)
  596. + await getStatsForCollection(
  597. + sampleSize,
  598. + 'projects',
  599. + projectsCollection,
  600. + { rootFolder: { $exists: true } },
  601. + { rootFolder: 1 },
  602. + projectsCountResult
  603. + )
  604. + await getStatsForCollection(
  605. + sampleSize,
  606. + 'deleted projects',
  607. + deletedProjectsCollection,
  608. + { 'project.rootFolder': { $exists: true } },
  609. + { 'project.rootFolder': 1 },
  610. + deletedProjectsCountResult
  611. + )
  612. +}
  613. +
  614. // Filestore endpoint location
  615. const FILESTORE_HOST = process.env.FILESTORE_HOST || '127.0.0.1'
  616. const FILESTORE_PORT = process.env.FILESTORE_PORT || '3009'
  617. @@ -1220,6 +1339,12 @@ async function main() {
  618. console.warn('Done.')
  619. }
  620. +if (DISPLAY_REPORT) {
  621. + console.warn('Displaying report...')
  622. + await displayReport()
  623. + process.exit(0)
  624. +}
  625. +
  626. try {
  627. try {
  628. await main()
  629. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  630. index b20e365c4ff..2bfc4051622 100644
  631. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  632. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  633. @@ -267,8 +267,20 @@ logger.initialize('file-migration', {
  634. },
  635. ],
  636. })
  637. +
  638. +let lastElapsedTime = 0
  639. async function trackProgress(progress) {
  640. - logger.info({}, progress)
  641. + const elapsedTime = Math.floor((performance.now() - processStart) / 1000)
  642. + if (lastElapsedTime === elapsedTime) {
  643. + // Avoid spamming the console with the same progress message
  644. + return
  645. + }
  646. + lastElapsedTime = elapsedTime
  647. + readline.clearLine(process.stdout, 0)
  648. + readline.cursorTo(process.stdout, 0)
  649. + process.stdout.write(
  650. + `Processed ${STATS.projects} projects, elapsed time ${elapsedTime}s`
  651. + )
  652. }
  653. /**
  654. @@ -1287,7 +1299,7 @@ async function processNonDeletedProjects() {
  655. } finally {
  656. await waitForDeferredQueues()
  657. }
  658. - console.warn('Done updating live projects')
  659. + console.warn('\nDone updating live projects')
  660. }
  661. async function processDeletedProjects() {
  662. @@ -1306,7 +1318,9 @@ async function processDeletedProjects() {
  663. 'project.rootFolder': 1,
  664. 'project._id': 1,
  665. 'project.overleaf.history.id': 1,
  666. - }
  667. + },
  668. + {},
  669. + { trackProgress }
  670. )
  671. } catch (err) {
  672. gracefulShutdownInitiated = true
  673. @@ -1314,7 +1328,7 @@ async function processDeletedProjects() {
  674. } finally {
  675. await waitForDeferredQueues()
  676. }
  677. - console.warn('Done updating deleted projects')
  678. + console.warn('\nDone updating deleted projects')
  679. }
  680. async function main() {
  681. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  682. index 2bfc4051622..c9fd7d233a7 100644
  683. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  684. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  685. @@ -94,9 +94,14 @@ function parseArgs() {
  686. { name: 'deleted-projects', type: Boolean },
  687. { name: 'include-hashed-files', type: Boolean },
  688. { name: 'skip-existing-blobs', type: Boolean },
  689. - { name: 'from-file', type: String, defaultValue: '' },
  690. - { name: 'dry-run', type: Boolean },
  691. - { name: 'output', type: String, defaultValue: DEFAULT_OUTPUT_FILE },
  692. + { name: 'from-file', alias: 'f', type: String, defaultValue: '' },
  693. + { name: 'dry-run', alias: 'n', type: Boolean },
  694. + {
  695. + name: 'output',
  696. + alias: 'o',
  697. + type: String,
  698. + defaultValue: DEFAULT_OUTPUT_FILE,
  699. + },
  700. { name: 'report', type: Boolean },
  701. {
  702. name: 'BATCH_RANGE_START',
  703. @@ -127,14 +132,14 @@ Project selection options:
  704. --all, -a Process all projects, including deleted ones
  705. --projects Process projects (excluding deleted ones)
  706. --deleted-projects Process deleted projects
  707. - --from-file <file> Process selected projects ids from file
  708. + --from-file <file>, -f <file> Process selected projects ids from file
  709. File selection options:
  710. --include-hashed-files Process files that already have a hash
  711. --skip-existing-blobs Skip processing files already in the blob store
  712. Logging options:
  713. - --output <file> Output log to the specified file
  714. + --output <file>, -o <file> Output log to the specified file
  715. (default: file-migration-<timestamp>.log)
  716. --logging-id <id> Identifier for logging
  717. (default: BATCH_RANGE_START)
  718. @@ -147,7 +152,7 @@ Batch range options:
  719. Other options:
  720. --report Display a report of the current status
  721. - --dry-run Perform a dry run without making changes
  722. + --dry-run, -n Perform a dry run without making changes
  723. --help, -h Show this help message
  724. Typical usage:
  725. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  726. index c9fd7d233a7..8f28e8a4d78 100644
  727. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  728. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  729. @@ -326,6 +326,7 @@ async function getStatsForCollection(
  730. for (const project of result) {
  731. const fileTree = JSON.stringify(project, [
  732. + 'project',
  733. 'rootFolder',
  734. 'folders',
  735. 'fileRefs',
  736. diff --git a/libraries/mongo-utils/batchedUpdate.js b/libraries/mongo-utils/batchedUpdate.js
  737. index 41af41f0d4a..f1253c587d3 100644
  738. --- a/libraries/mongo-utils/batchedUpdate.js
  739. +++ b/libraries/mongo-utils/batchedUpdate.js
  740. @@ -35,7 +35,7 @@ let BATCHED_UPDATE_RUNNING = false
  741. * @property {string} [BATCH_RANGE_START]
  742. * @property {string} [BATCH_SIZE]
  743. * @property {string} [VERBOSE_LOGGING]
  744. - * @property {(progress: string) => Promise<void>} [trackProgress]
  745. + * @property {(progress: string, options?: object) => Promise<void>} [trackProgress]
  746. */
  747. /**
  748. @@ -269,9 +269,12 @@ async function batchedUpdate(
  749. await performUpdate(collection, nextBatch, update)
  750. }
  751. }
  752. - await trackProgress(`Completed batch ending ${renderObjectId(end)}`)
  753. + await trackProgress(`Completed batch ending ${renderObjectId(end)}`, {
  754. + completedBatch: true,
  755. + })
  756. start = end
  757. }
  758. + await trackProgress('Completed all batches', { completedAll: true })
  759. return updated
  760. } finally {
  761. BATCHED_UPDATE_RUNNING = false
  762. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  763. index 8f28e8a4d78..2b54fdb1687 100644
  764. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  765. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  766. @@ -274,9 +274,16 @@ logger.initialize('file-migration', {
  767. })
  768. let lastElapsedTime = 0
  769. -async function trackProgress(progress) {
  770. +async function trackProgress(progress, options = {}) {
  771. + if (OUTPUT_FILE === '-') {
  772. + return // skip progress tracking when logging to stdout
  773. + }
  774. + if (options.completedAll) {
  775. + process.stdout.write('\n')
  776. + return
  777. + }
  778. const elapsedTime = Math.floor((performance.now() - processStart) / 1000)
  779. - if (lastElapsedTime === elapsedTime) {
  780. + if (lastElapsedTime === elapsedTime && !options.completedBatch) {
  781. // Avoid spamming the console with the same progress message
  782. return
  783. }
  784. @@ -1305,7 +1312,7 @@ async function processNonDeletedProjects() {
  785. } finally {
  786. await waitForDeferredQueues()
  787. }
  788. - console.warn('\nDone updating live projects')
  789. + console.warn('Done updating live projects')
  790. }
  791. async function processDeletedProjects() {
  792. @@ -1334,7 +1341,7 @@ async function processDeletedProjects() {
  793. } finally {
  794. await waitForDeferredQueues()
  795. }
  796. - console.warn('\nDone updating deleted projects')
  797. + console.warn('Done updating deleted projects')
  798. }
  799. async function main() {
  800. @@ -1381,7 +1388,9 @@ try {
  801. let code = 0
  802. if (STATS.filesFailed > 0) {
  803. - console.warn('Some files could not be processed, see logs and try again')
  804. + console.warn(
  805. + `Some files could not be processed, see logs in ${OUTPUT_FILE} and try again`
  806. + )
  807. code++
  808. }
  809. if (STATS.fileHardDeleted > 0) {
  810. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  811. index 2b54fdb1687..fc46f245d1a 100644
  812. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  813. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  814. @@ -525,7 +525,7 @@ function computeDiff(nextEventLoopStats, now) {
  815. function printStats(isLast = false) {
  816. const now = performance.now()
  817. const nextEventLoopStats = performance.eventLoopUtilization()
  818. - const logLine = JSON.stringify({
  819. + const logLine = {
  820. time: new Date(),
  821. LOGGING_IDENTIFIER,
  822. ...STATS,
  823. @@ -533,11 +533,11 @@ function printStats(isLast = false) {
  824. eventLoop: nextEventLoopStats,
  825. diff: computeDiff(nextEventLoopStats, now),
  826. deferredBatches: Array.from(deferredBatches.keys()),
  827. - })
  828. - if (isLast) {
  829. - console.warn(logLine)
  830. + }
  831. + if (isLast && OUTPUT_FILE === '-') {
  832. + console.warn(JSON.stringify(logLine))
  833. } else {
  834. - console.log(logLine)
  835. + logger.info(logLine, 'file-migration stats')
  836. }
  837. lastEventLoopStats = nextEventLoopStats
  838. lastLog = Object.assign({}, STATS)
  839. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  840. index fc46f245d1a..4a4d93d902c 100644
  841. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  842. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  843. @@ -92,7 +92,7 @@ function parseArgs() {
  844. { name: 'all', alias: 'a', type: Boolean },
  845. { name: 'projects', type: Boolean },
  846. { name: 'deleted-projects', type: Boolean },
  847. - { name: 'include-hashed-files', type: Boolean },
  848. + { name: 'skip-hashed-files', type: Boolean },
  849. { name: 'skip-existing-blobs', type: Boolean },
  850. { name: 'from-file', alias: 'f', type: String, defaultValue: '' },
  851. { name: 'dry-run', alias: 'n', type: Boolean },
  852. @@ -135,7 +135,7 @@ Project selection options:
  853. --from-file <file>, -f <file> Process selected projects ids from file
  854. File selection options:
  855. - --include-hashed-files Process files that already have a hash
  856. + --skip-hashed-files Skip processing files that already have a hash
  857. --skip-existing-blobs Skip processing files already in the blob store
  858. Logging options:
  859. @@ -161,8 +161,7 @@ Typical usage:
  860. is equivalent to
  861. - node back_fill_file_hash.mjs --projects --deleted-projects \\
  862. - --include-hashed-files
  863. + node back_fill_file_hash.mjs --projects --deleted-projects
  864. `)
  865. process.exit(0)
  866. }
  867. @@ -199,7 +198,6 @@ is equivalent to
  868. if (args.all) {
  869. args.projects = true
  870. args['deleted-projects'] = true
  871. - args['include-hashed-files'] = true
  872. }
  873. const BATCH_RANGE_START = objectIdFromInput(args.BATCH_RANGE_START).toString()
  874. @@ -207,7 +205,7 @@ is equivalent to
  875. return {
  876. PROCESS_NON_DELETED_PROJECTS: args.projects,
  877. PROCESS_DELETED_PROJECTS: args['deleted-projects'],
  878. - PROCESS_HASHED_FILES: args['include-hashed-files'],
  879. + PROCESS_HASHED_FILES: !args['skip-hashed-files'],
  880. PROCESS_BLOBS: !args['skip-existing-blobs'],
  881. DRY_RUN: args['dry-run'],
  882. OUTPUT_FILE: args.output,
  883. 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
  884. index 117352d6164..a95bcbabd7e 100644
  885. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  886. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  887. @@ -855,7 +855,7 @@ describe('back_fill_file_hash script', function () {
  888. // Practically, this is slow and moving it to the end of the tests gets us there most of the way.
  889. it('should process nothing on re-run', async function () {
  890. const rerun = await runScript(
  891. - processHashedFiles ? ['--include-hashed-files'] : [],
  892. + !processHashedFiles ? ['--skip-hashed-files'] : [],
  893. {},
  894. false
  895. )
  896. @@ -981,7 +981,7 @@ describe('back_fill_file_hash script', function () {
  897. it('should gracefully handle fatal errors', async function () {
  898. mockFilestore.deleteObject(projectId0, fileId0)
  899. const t0 = Date.now()
  900. - const { stats, result } = await tryRunScript([], {
  901. + const { stats, result } = await tryRunScript(['--skip-hashed-files'], {
  902. RETRIES: '10',
  903. RETRY_DELAY_MS: '1000',
  904. })
  905. @@ -1016,7 +1016,7 @@ describe('back_fill_file_hash script', function () {
  906. value: { stats, result },
  907. },
  908. ] = await Promise.allSettled([
  909. - tryRunScript([], {
  910. + tryRunScript(['--skip-hashed-files'], {
  911. RETRY_DELAY_MS: '100',
  912. RETRIES: '60',
  913. RETRY_FILESTORE_404: 'true', // 404s are the easiest to simulate in tests
  914. @@ -1042,7 +1042,7 @@ describe('back_fill_file_hash script', function () {
  915. let output
  916. before('prepare environment', prepareEnvironment)
  917. before('run script', async function () {
  918. - output = await runScript([], {
  919. + output = await runScript(['--skip-hashed-files'], {
  920. CONCURRENCY: '1',
  921. })
  922. })
  923. @@ -1111,10 +1111,10 @@ describe('back_fill_file_hash script', function () {
  924. let output1, output2
  925. before('prepare environment', prepareEnvironment)
  926. before('run script without hashed files', async function () {
  927. - output1 = await runScript([], {})
  928. + output1 = await runScript(['--skip-hashed-files'], {})
  929. })
  930. before('run script with hashed files', async function () {
  931. - output2 = await runScript(['--include-hashed-files'], {})
  932. + output2 = await runScript([], {})
  933. })
  934. it('should print stats for the first run without hashed files', function () {
  935. expect(output1.stats).deep.equal(STATS_ALL)
  936. @@ -1134,7 +1134,7 @@ describe('back_fill_file_hash script', function () {
  937. let output
  938. before('prepare environment', prepareEnvironment)
  939. before('run script', async function () {
  940. - output = await runScript([], {
  941. + output = await runScript(['--skip-hashed-files'], {
  942. CONCURRENCY: '10',
  943. })
  944. })
  945. @@ -1148,7 +1148,7 @@ describe('back_fill_file_hash script', function () {
  946. let output
  947. before('prepare environment', prepareEnvironment)
  948. before('run script', async function () {
  949. - output = await runScript([], {
  950. + output = await runScript(['--skip-hashed-files'], {
  951. STREAM_HIGH_WATER_MARK: (1024 * 1024).toString(),
  952. })
  953. })
  954. @@ -1162,7 +1162,7 @@ describe('back_fill_file_hash script', function () {
  955. let output
  956. before('prepare environment', prepareEnvironment)
  957. before('run script', async function () {
  958. - output = await runScript(['--include-hashed-files'], {})
  959. + output = await runScript([], {})
  960. })
  961. it('should print stats', function () {
  962. expect(output.stats).deep.equal(
  963. @@ -1191,7 +1191,7 @@ describe('back_fill_file_hash script', function () {
  964. })
  965. let output
  966. before('run script', async function () {
  967. - output = await runScript([], {
  968. + output = await runScript(['--skip-hashed-files'], {
  969. CONCURRENCY: '1',
  970. })
  971. })
  972. @@ -1212,14 +1212,20 @@ describe('back_fill_file_hash script', function () {
  973. let outputPart0, outputPart1
  974. before('prepare environment', prepareEnvironment)
  975. before('run script on part 0', async function () {
  976. - outputPart0 = await runScript([`--BATCH_RANGE_END=${edge}`], {
  977. - CONCURRENCY: '1',
  978. - })
  979. + outputPart0 = await runScript(
  980. + ['--skip-hashed-files', `--BATCH_RANGE_END=${edge}`],
  981. + {
  982. + CONCURRENCY: '1',
  983. + }
  984. + )
  985. })
  986. before('run script on part 1', async function () {
  987. - outputPart1 = await runScript([`--BATCH_RANGE_START=${edge}`], {
  988. - CONCURRENCY: '1',
  989. - })
  990. + outputPart1 = await runScript(
  991. + ['--skip-hashed-files', `--BATCH_RANGE_START=${edge}`],
  992. + {
  993. + CONCURRENCY: '1',
  994. + }
  995. + )
  996. })
  997. it('should print stats for part 0', function () {
  998. @@ -1264,10 +1270,16 @@ describe('back_fill_file_hash script', function () {
  999. let outputPart0, outputPart1
  1000. before('run script on part 0', async function () {
  1001. - outputPart0 = await runScript([`--from-file=${path0}`])
  1002. + outputPart0 = await runScript([
  1003. + '--skip-hashed-files',
  1004. + `--from-file=${path0}`,
  1005. + ])
  1006. })
  1007. before('run script on part 1', async function () {
  1008. - outputPart1 = await runScript([`--from-file=${path1}`])
  1009. + outputPart1 = await runScript([
  1010. + '--skip-hashed-files',
  1011. + `--from-file=${path1}`,
  1012. + ])
  1013. })
  1014. /**
  1015. 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
  1016. index a95bcbabd7e..fc6941bd7bb 100644
  1017. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  1018. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  1019. @@ -975,7 +975,7 @@ describe('back_fill_file_hash script', function () {
  1020. STATS_UP_FROM_PROJECT1_ONWARD
  1021. )
  1022. - describe('error cases', () => {
  1023. + describe('error cases', function () {
  1024. beforeEach('prepare environment', prepareEnvironment)
  1025. it('should gracefully handle fatal errors', async function () {
  1026. @@ -1237,7 +1237,7 @@ describe('back_fill_file_hash script', function () {
  1027. commonAssertions()
  1028. })
  1029. - describe('projectIds from file', () => {
  1030. + describe('projectIds from file', function () {
  1031. const path0 = '/tmp/project-ids-0.txt'
  1032. const path1 = '/tmp/project-ids-1.txt'
  1033. before('prepare environment', prepareEnvironment)
  1034. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1035. index 4a4d93d902c..375e582c331 100644
  1036. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1037. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1038. @@ -555,7 +555,7 @@ function handleSignal() {
  1039. /**
  1040. * @param {QueueEntry} entry
  1041. - * @return {Promise<string>}
  1042. + * @return {Promise<string|undefined>}
  1043. */
  1044. async function processFileWithCleanup(entry) {
  1045. const {
  1046. @@ -578,7 +578,7 @@ async function processFileWithCleanup(entry) {
  1047. /**
  1048. * @param {QueueEntry} entry
  1049. * @param {string} filePath
  1050. - * @return {Promise<string>}
  1051. + * @return {Promise<string|undefined>}
  1052. */
  1053. async function processFile(entry, filePath) {
  1054. for (let attempt = 0; attempt < RETRIES; attempt++) {
  1055. @@ -612,7 +612,7 @@ async function processFile(entry, filePath) {
  1056. /**
  1057. * @param {QueueEntry} entry
  1058. * @param {string} filePath
  1059. - * @return {Promise<string>}
  1060. + * @return {Promise<string|undefined>}
  1061. */
  1062. async function processFileOnce(entry, filePath) {
  1063. const {
  1064. @@ -627,10 +627,7 @@ async function processFileOnce(entry, filePath) {
  1065. return entry.hash
  1066. }
  1067. if (DRY_RUN) {
  1068. - console.log(
  1069. - `DRY-RUN: would process file ${fileId} for project ${projectId}`
  1070. - )
  1071. - return 'dry-run'
  1072. + return // skip processing in dry-run mode by returning undefined
  1073. }
  1074. const blobStore = new BlobStore(historyId)
  1075. STATS.readFromGCSCount++
  1076. @@ -843,6 +840,9 @@ async function handleDeletedFileTreeBatch(batch) {
  1077. * @return {Promise<boolean>}
  1078. */
  1079. async function tryUpdateFileRefInMongo(entry) {
  1080. + if (DRY_RUN) {
  1081. + return true // skip mongo updates in dry-run mode
  1082. + }
  1083. if (entry.path.startsWith('project.')) {
  1084. return await tryUpdateFileRefInMongoInDeletedProject(entry)
  1085. }
  1086. @@ -865,6 +865,9 @@ async function tryUpdateFileRefInMongo(entry) {
  1087. * @return {Promise<boolean>}
  1088. */
  1089. async function tryUpdateFileRefInMongoInDeletedProject(entry) {
  1090. + if (DRY_RUN) {
  1091. + return true // skip mongo updates in dry-run mode
  1092. + }
  1093. STATS.mongoUpdates++
  1094. const result = await deletedProjectsCollection.updateOne(
  1095. {
  1096. @@ -1165,6 +1168,7 @@ class ProjectContext {
  1097. */
  1098. async #tryBatchHashWrites(collection, entries, query) {
  1099. if (entries.length === 0) return []
  1100. + if (DRY_RUN) return [] // skip mongo updates in dry-run mode
  1101. const update = {}
  1102. for (const entry of entries) {
  1103. query[`${entry.path}._id`] = new ObjectId(entry.fileId)
  1104. @@ -1210,7 +1214,7 @@ class ProjectContext {
  1105. }
  1106. }
  1107. - /** @type {Map<string, Promise<string>>} */
  1108. + /** @type {Map<string, Promise<string|undefined>>} */
  1109. #pendingFiles = new Map()
  1110. /**
  1111. @@ -1223,7 +1227,12 @@ class ProjectContext {
  1112. this.#pendingFiles.set(entry.cacheKey, processFileWithCleanup(entry))
  1113. }
  1114. try {
  1115. - entry.hash = await this.#pendingFiles.get(entry.cacheKey)
  1116. + const hash = await this.#pendingFiles.get(entry.cacheKey)
  1117. + if (!hash) {
  1118. + return // hash is undefined in dry-run mode
  1119. + } else {
  1120. + entry.hash = hash
  1121. + }
  1122. } finally {
  1123. this.remainingQueueEntries--
  1124. }
  1125. 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
  1126. index fc6941bd7bb..646e75e2b58 100644
  1127. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  1128. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  1129. @@ -1130,6 +1130,45 @@ describe('back_fill_file_hash script', function () {
  1130. commonAssertions(true)
  1131. })
  1132. + describe('full run in dry-run mode', function () {
  1133. + let output
  1134. + before('prepare environment', prepareEnvironment)
  1135. + before('run script', async function () {
  1136. + output = await runScript(
  1137. + ['--dry-run'],
  1138. + {
  1139. + CONCURRENCY: '1',
  1140. + },
  1141. + false
  1142. + )
  1143. + })
  1144. +
  1145. + it('should print stats for dry-run mode', function () {
  1146. + // Compute the stats for running the script without dry-run mode.
  1147. + const originalStats = sumStats(STATS_ALL, {
  1148. + ...STATS_FILES_HASHED_EXTRA,
  1149. + readFromGCSCount: 30,
  1150. + readFromGCSIngress: 72,
  1151. + mongoUpdates: 0,
  1152. + filesWithHash: 3,
  1153. + })
  1154. + // For a dry-run mode, we expect the stats to be zero except for the
  1155. + // count of projects, blobs, bad file trees, duplicated files
  1156. + // and files with/without hash. All the other stats such as mongoUpdates
  1157. + // and writeToGCSCount, etc should be zero.
  1158. + const expectedDryRunStats = {
  1159. + ...STATS_ALL_ZERO,
  1160. + projects: originalStats.projects,
  1161. + blobs: originalStats.blobs,
  1162. + badFileTrees: originalStats.badFileTrees,
  1163. + filesDuplicated: originalStats.filesDuplicated,
  1164. + filesWithHash: originalStats.filesWithHash,
  1165. + filesWithoutHash: originalStats.filesWithoutHash,
  1166. + }
  1167. + expect(output.stats).deep.equal(expectedDryRunStats)
  1168. + })
  1169. + })
  1170. +
  1171. describe('full run CONCURRENCY=10', function () {
  1172. let output
  1173. before('prepare environment', prepareEnvironment)
  1174. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1175. index 375e582c331..85920bcf03a 100644
  1176. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1177. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1178. @@ -94,7 +94,7 @@ function parseArgs() {
  1179. { name: 'deleted-projects', type: Boolean },
  1180. { name: 'skip-hashed-files', type: Boolean },
  1181. { name: 'skip-existing-blobs', type: Boolean },
  1182. - { name: 'from-file', alias: 'f', type: String, defaultValue: '' },
  1183. + { name: 'from-file', type: String, defaultValue: '' },
  1184. { name: 'dry-run', alias: 'n', type: Boolean },
  1185. {
  1186. name: 'output',
  1187. @@ -132,7 +132,7 @@ Project selection options:
  1188. --all, -a Process all projects, including deleted ones
  1189. --projects Process projects (excluding deleted ones)
  1190. --deleted-projects Process deleted projects
  1191. - --from-file <file>, -f <file> Process selected projects ids from file
  1192. + --from-file <file> Process selected projects ids from file
  1193. File selection options:
  1194. --skip-hashed-files Skip processing files that already have a hash
  1195. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1196. index 85920bcf03a..092b8f04e43 100644
  1197. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1198. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1199. @@ -567,10 +567,7 @@ async function processFileWithCleanup(entry) {
  1200. return await processFile(entry, filePath)
  1201. } finally {
  1202. if (!DRY_RUN) {
  1203. - await Promise.all([
  1204. - fs.promises.rm(filePath, { force: true }),
  1205. - fs.promises.rm(filePath + GZ_SUFFIX, { force: true }),
  1206. - ])
  1207. + await fs.promises.rm(filePath, { force: true })
  1208. }
  1209. }
  1210. }
  1211. @@ -697,8 +694,6 @@ async function uploadBlobToGCS(blobStore, entry, blob, hash, filePath) {
  1212. entry.ctx.recordHistoryBlob(blob)
  1213. }
  1214. -const GZ_SUFFIX = '.gz'
  1215. -
  1216. /**
  1217. * @param {Array<QueueEntry>} files
  1218. * @return {Promise<void>}
  1219. diff --git a/libraries/mongo-utils/batchedUpdate.js b/libraries/mongo-utils/batchedUpdate.js
  1220. index f1253c587d3..41af41f0d4a 100644
  1221. --- a/libraries/mongo-utils/batchedUpdate.js
  1222. +++ b/libraries/mongo-utils/batchedUpdate.js
  1223. @@ -35,7 +35,7 @@ let BATCHED_UPDATE_RUNNING = false
  1224. * @property {string} [BATCH_RANGE_START]
  1225. * @property {string} [BATCH_SIZE]
  1226. * @property {string} [VERBOSE_LOGGING]
  1227. - * @property {(progress: string, options?: object) => Promise<void>} [trackProgress]
  1228. + * @property {(progress: string) => Promise<void>} [trackProgress]
  1229. */
  1230. /**
  1231. @@ -269,12 +269,9 @@ async function batchedUpdate(
  1232. await performUpdate(collection, nextBatch, update)
  1233. }
  1234. }
  1235. - await trackProgress(`Completed batch ending ${renderObjectId(end)}`, {
  1236. - completedBatch: true,
  1237. - })
  1238. + await trackProgress(`Completed batch ending ${renderObjectId(end)}`)
  1239. start = end
  1240. }
  1241. - await trackProgress('Completed all batches', { completedAll: true })
  1242. return updated
  1243. } finally {
  1244. BATCHED_UPDATE_RUNNING = false
  1245. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1246. index 092b8f04e43..755443adf52 100644
  1247. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1248. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1249. @@ -1305,7 +1305,7 @@ async function processNonDeletedProjects() {
  1250. {
  1251. BATCH_RANGE_START,
  1252. BATCH_RANGE_END,
  1253. - trackProgress,
  1254. + trackProgress: async message => {},
  1255. }
  1256. )
  1257. } catch (err) {
  1258. @@ -1335,7 +1335,7 @@ async function processDeletedProjects() {
  1259. 'project.overleaf.history.id': 1,
  1260. },
  1261. {},
  1262. - { trackProgress }
  1263. + { trackProgress: async message => {} }
  1264. )
  1265. } catch (err) {
  1266. gracefulShutdownInitiated = true
  1267. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1268. index 755443adf52..4ca17ddf694 100644
  1269. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1270. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1271. @@ -272,7 +272,7 @@ logger.initialize('file-migration', {
  1272. })
  1273. let lastElapsedTime = 0
  1274. -async function trackProgress(progress, options = {}) {
  1275. +async function displayProgress(options = {}) {
  1276. if (OUTPUT_FILE === '-') {
  1277. return // skip progress tracking when logging to stdout
  1278. }
  1279. @@ -733,6 +733,7 @@ async function waitForDeferredQueues() {
  1280. // Wait for ALL pending batches to finish, especially wait for their mongo
  1281. // writes to finish to avoid extra work when resuming the batch.
  1282. const all = await Promise.allSettled(deferredBatches.values())
  1283. + displayProgress({ completedAll: true })
  1284. // Now that all batches finished, we can throw if needed.
  1285. for (const res of all) {
  1286. if (res.status === 'rejected') {
  1287. @@ -756,6 +757,7 @@ async function queueNextBatch(batch, prefix = 'rootFolder.0') {
  1288. const deferred = processBatch(batch, prefix)
  1289. .then(() => {
  1290. logger.info({ end }, 'actually completed batch')
  1291. + displayProgress({ completedBatch: true })
  1292. })
  1293. .catch(err => {
  1294. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1295. index 4ca17ddf694..8664be21fbe 100644
  1296. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1297. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  1298. @@ -1226,7 +1226,11 @@ class ProjectContext {
  1299. try {
  1300. const hash = await this.#pendingFiles.get(entry.cacheKey)
  1301. if (!hash) {
  1302. - return // hash is undefined in dry-run mode
  1303. + if (DRY_RUN) {
  1304. + return // hash is undefined in dry-run mode
  1305. + } else {
  1306. + throw new Error('undefined hash outside dry-run mode')
  1307. + }
  1308. } else {
  1309. entry.hash = hash
  1310. }
  1311. 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
  1312. index 646e75e2b58..43884adbe8f 100644
  1313. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  1314. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  1315. @@ -1132,7 +1132,15 @@ describe('back_fill_file_hash script', function () {
  1316. describe('full run in dry-run mode', function () {
  1317. let output
  1318. + let projectRecordsBefore
  1319. + let deletedProjectRecordsBefore
  1320. before('prepare environment', prepareEnvironment)
  1321. + before(async function () {
  1322. + projectRecordsBefore = await projectsCollection.find({}).toArray()
  1323. + deletedProjectRecordsBefore = await deletedProjectsCollection
  1324. + .find({})
  1325. + .toArray()
  1326. + })
  1327. before('run script', async function () {
  1328. output = await runScript(
  1329. ['--dry-run'],
  1330. @@ -1167,6 +1175,14 @@ describe('back_fill_file_hash script', function () {
  1331. }
  1332. expect(output.stats).deep.equal(expectedDryRunStats)
  1333. })
  1334. + it('should not update mongo', async function () {
  1335. + expect(await projectsCollection.find({}).toArray()).to.deep.equal(
  1336. + projectRecordsBefore
  1337. + )
  1338. + expect(await deletedProjectsCollection.find({}).toArray()).to.deep.equal(
  1339. + deletedProjectRecordsBefore
  1340. + )
  1341. + })
  1342. })
  1343. describe('full run CONCURRENCY=10', function () {