pr_27147.patch 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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 ba3e0d43598e..feb4612ddc23 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. @@ -33,7 +33,6 @@ import {
  6. makeProjectKey,
  7. } from '../lib/blob_store/index.js'
  8. import { backedUpBlobs as backedUpBlobsCollection, db } from '../lib/mongodb.js'
  9. -import filestorePersistor from '../lib/persistor.js'
  10. import commandLineArgs from 'command-line-args'
  11. import readline from 'node:readline'
  12. @@ -179,6 +178,37 @@ const STREAM_HIGH_WATER_MARK = parseInt(
  13. const LOGGING_INTERVAL = parseInt(process.env.LOGGING_INTERVAL || '60000', 10)
  14. const SLEEP_BEFORE_EXIT = parseInt(process.env.SLEEP_BEFORE_EXIT || '1000', 10)
  15. +// Filestore endpoint location, the port is always hardcoded
  16. +const FILESTORE_HOST = process.env.FILESTORE_HOST || '127.0.0.1'
  17. +const FILESTORE_PORT = process.env.FILESTORE_PORT || '3009'
  18. +
  19. +async function fetchFromFilestore(projectId, fileId) {
  20. + const url = `http://${FILESTORE_HOST}:${FILESTORE_PORT}/project/${projectId}/file/${fileId}`
  21. + const response = await fetch(url)
  22. + if (!response.ok) {
  23. + if (response.status === 404) {
  24. + throw new NotFoundError('file not found in filestore', {
  25. + status: response.status,
  26. + })
  27. + }
  28. + const body = await response.text()
  29. + throw new OError('fetchFromFilestore failed', {
  30. + projectId,
  31. + fileId,
  32. + status: response.status,
  33. + body,
  34. + })
  35. + }
  36. + if (!response.body) {
  37. + throw new OError('fetchFromFilestore response has no body', {
  38. + projectId,
  39. + fileId,
  40. + status: response.status,
  41. + })
  42. + }
  43. + return response.body
  44. +}
  45. +
  46. const projectsCollection = db.collection('projects')
  47. /** @type {ProjectsCollection} */
  48. const typedProjectsCollection = db.collection('projects')
  49. @@ -348,8 +378,7 @@ async function processFile(entry, filePath) {
  50. } catch (err) {
  51. if (gracefulShutdownInitiated) throw err
  52. if (err instanceof NotFoundError) {
  53. - const { bucketName } = OError.getFullInfo(err)
  54. - if (bucketName === USER_FILES_BUCKET_NAME && !RETRY_FILESTORE_404) {
  55. + if (!RETRY_FILESTORE_404) {
  56. throw err // disable retries for not found in filestore bucket case
  57. }
  58. }
  59. @@ -416,10 +445,8 @@ async function processFileOnce(entry, filePath) {
  60. }
  61. STATS.readFromGCSCount++
  62. - const src = await filestorePersistor.getObjectStream(
  63. - USER_FILES_BUCKET_NAME,
  64. - `${projectId}/${fileId}`
  65. - )
  66. + // make a fetch request to filestore itself
  67. + const src = await fetchFromFilestore(projectId, fileId)
  68. const dst = fs.createWriteStream(filePath, {
  69. highWaterMark: STREAM_HIGH_WATER_MARK,
  70. })
  71. @@ -1327,14 +1354,21 @@ async function processDeletedProjects() {
  72. }
  73. async function main() {
  74. + console.log('Starting project file backup...')
  75. await loadGlobalBlobs()
  76. + console.log('Loaded global blobs:', GLOBAL_BLOBS.size)
  77. if (PROJECT_IDS_FROM) {
  78. + console.log(
  79. + `Processing projects from file: ${PROJECT_IDS_FROM}, this may take a while...`
  80. + )
  81. await processProjectsFromFile()
  82. } else {
  83. if (PROCESS_NON_DELETED_PROJECTS) {
  84. + console.log('Processing non-deleted projects...')
  85. await processNonDeletedProjects()
  86. }
  87. if (PROCESS_DELETED_PROJECTS) {
  88. + console.log('Processing deleted projects...')
  89. await processDeletedProjects()
  90. }
  91. }
  92. 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
  93. index fd39369a7189..4e697b8bec2c 100644
  94. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  95. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  96. @@ -15,7 +15,6 @@ import { execFile } from 'node:child_process'
  97. import chai, { expect } from 'chai'
  98. import chaiExclude from 'chai-exclude'
  99. import config from 'config'
  100. -import ObjectPersistor from '@overleaf/object-persistor'
  101. import { WritableBuffer } from '@overleaf/stream-utils'
  102. import {
  103. backupPersistor,
  104. @@ -27,6 +26,9 @@ import {
  105. makeProjectKey,
  106. } from '../../../../storage/lib/blob_store/index.js'
  107. +import express from 'express'
  108. +import bodyParser from 'body-parser'
  109. +
  110. chai.use(chaiExclude)
  111. const TIMEOUT = 20 * 1_000
  112. @@ -36,15 +38,60 @@ const { tieringStorageClass } = config.get('backupPersistor')
  113. const projectsCollection = db.collection('projects')
  114. const deletedProjectsCollection = db.collection('deletedProjects')
  115. -const FILESTORE_PERSISTOR = ObjectPersistor({
  116. - backend: 'gcs',
  117. - gcs: {
  118. - endpoint: {
  119. - apiEndpoint: process.env.GCS_API_ENDPOINT,
  120. - projectId: process.env.GCS_PROJECT_ID,
  121. - },
  122. - },
  123. -})
  124. +class MockFilestore {
  125. + constructor() {
  126. + this.host = process.env.FILESTORE_HOST || '127.0.0.1'
  127. + this.port = process.env.FILESTORE_PORT || 3009
  128. + // create a server listening on this.host and this.port
  129. + this.files = {}
  130. +
  131. + this.app = express()
  132. + this.app.use(bodyParser.json())
  133. + this.app.use(bodyParser.urlencoded({ extended: true }))
  134. +
  135. + this.app.get('/project/:projectId/file/:fileId', (req, res) => {
  136. + const { projectId, fileId } = req.params
  137. + const content = this.files[projectId]?.[fileId]
  138. + if (!content) return res.status(404).end()
  139. + res.status(200).end(content)
  140. + })
  141. + }
  142. +
  143. + start() {
  144. + // reset stored files
  145. + this.files = {}
  146. + // start the server
  147. + if (this.serverPromise) {
  148. + return this.serverPromise
  149. + } else {
  150. + this.serverPromise = new Promise((resolve, reject) => {
  151. + this.server = this.app.listen(this.port, this.host, err => {
  152. + if (err) return reject(err)
  153. + resolve()
  154. + })
  155. + })
  156. + return this.serverPromise
  157. + }
  158. + }
  159. +
  160. + addFile(projectId, fileId, fileContent) {
  161. + if (!this.files[projectId]) {
  162. + this.files[projectId] = {}
  163. + }
  164. + this.files[projectId][fileId] = fileContent
  165. + }
  166. +
  167. + deleteObject(projectId, fileId) {
  168. + if (this.files[projectId]) {
  169. + delete this.files[projectId][fileId]
  170. + if (Object.keys(this.files[projectId]).length === 0) {
  171. + delete this.files[projectId]
  172. + }
  173. + }
  174. + }
  175. +}
  176. +
  177. +const mockFilestore = new MockFilestore()
  178. /**
  179. * @param {ObjectId} objectId
  180. @@ -472,67 +519,36 @@ describe('back_fill_file_hash script', function () {
  181. }
  182. async function populateFilestore() {
  183. - await FILESTORE_PERSISTOR.sendStream(
  184. - USER_FILES_BUCKET_NAME,
  185. - `${projectId0}/${fileId0}`,
  186. - Stream.Readable.from([fileId0.toString()])
  187. - )
  188. - await FILESTORE_PERSISTOR.sendStream(
  189. - USER_FILES_BUCKET_NAME,
  190. - `${projectId0}/${fileId6}`,
  191. - Stream.Readable.from([fileId6.toString()])
  192. - )
  193. - await FILESTORE_PERSISTOR.sendStream(
  194. - USER_FILES_BUCKET_NAME,
  195. - `${projectId0}/${fileId7}`,
  196. - Stream.Readable.from([contentFile7])
  197. - )
  198. - await FILESTORE_PERSISTOR.sendStream(
  199. - USER_FILES_BUCKET_NAME,
  200. - `${projectId1}/${fileId1}`,
  201. - Stream.Readable.from([fileId1.toString()])
  202. - )
  203. - await FILESTORE_PERSISTOR.sendStream(
  204. - USER_FILES_BUCKET_NAME,
  205. - `${projectId2}/${fileId2}`,
  206. - Stream.Readable.from([fileId2.toString()])
  207. - )
  208. - await FILESTORE_PERSISTOR.sendStream(
  209. - USER_FILES_BUCKET_NAME,
  210. - `${projectId3}/${fileId3}`,
  211. - Stream.Readable.from([fileId3.toString()])
  212. - )
  213. - await FILESTORE_PERSISTOR.sendStream(
  214. - USER_FILES_BUCKET_NAME,
  215. - `${projectId3}/${fileId10}`,
  216. + await mockFilestore.addFile(projectId0, fileId0, fileId0.toString())
  217. + await mockFilestore.addFile(projectId0, fileId6, fileId6.toString())
  218. + await mockFilestore.addFile(projectId0, fileId7, contentFile7)
  219. + await mockFilestore.addFile(projectId1, fileId1, fileId1.toString())
  220. + await mockFilestore.addFile(projectId2, fileId2, fileId2.toString())
  221. + await mockFilestore.addFile(projectId3, fileId3, fileId3.toString())
  222. + await mockFilestore.addFile(
  223. + projectId3,
  224. + fileId10,
  225. // fileId10 is dupe of fileId3
  226. - Stream.Readable.from([fileId3.toString()])
  227. + fileId3.toString()
  228. )
  229. - await FILESTORE_PERSISTOR.sendStream(
  230. - USER_FILES_BUCKET_NAME,
  231. - `${projectId3}/${fileId11}`,
  232. + await mockFilestore.addFile(
  233. + projectId3,
  234. + fileId11,
  235. // fileId11 is dupe of fileId3
  236. - Stream.Readable.from([fileId3.toString()])
  237. - )
  238. - await FILESTORE_PERSISTOR.sendStream(
  239. - USER_FILES_BUCKET_NAME,
  240. - `${projectIdDeleted0}/${fileId4}`,
  241. - Stream.Readable.from([fileId4.toString()])
  242. + fileId3.toString()
  243. )
  244. - await FILESTORE_PERSISTOR.sendStream(
  245. - USER_FILES_BUCKET_NAME,
  246. - `${projectIdDeleted1}/${fileId5}`,
  247. - Stream.Readable.from([fileId5.toString()])
  248. - )
  249. - await FILESTORE_PERSISTOR.sendStream(
  250. - USER_FILES_BUCKET_NAME,
  251. - `${projectIdBadFileTree3}/${fileId9}`,
  252. - Stream.Readable.from([fileId9.toString()])
  253. + await mockFilestore.addFile(projectIdDeleted0, fileId4, fileId4.toString())
  254. + await mockFilestore.addFile(projectIdDeleted1, fileId5, fileId5.toString())
  255. + await mockFilestore.addFile(
  256. + projectIdBadFileTree3,
  257. + fileId9,
  258. + fileId9.toString()
  259. )
  260. }
  261. async function prepareEnvironment() {
  262. await cleanup.everything()
  263. + await mockFilestore.start()
  264. await populateMongo()
  265. await populateHistoryV1()
  266. await populateFilestore()
  267. @@ -1117,10 +1133,7 @@ describe('back_fill_file_hash script', function () {
  268. beforeEach('prepare environment', prepareEnvironment)
  269. it('should gracefully handle fatal errors', async function () {
  270. - await FILESTORE_PERSISTOR.deleteObject(
  271. - USER_FILES_BUCKET_NAME,
  272. - `${projectId0}/${fileId0}`
  273. - )
  274. + mockFilestore.deleteObject(projectId0, fileId0)
  275. const t0 = Date.now()
  276. const { stats, result } = await tryRunScript([], {
  277. RETRIES: '10',
  278. @@ -1148,17 +1161,10 @@ describe('back_fill_file_hash script', function () {
  279. })
  280. it('should retry on error', async function () {
  281. - await FILESTORE_PERSISTOR.deleteObject(
  282. - USER_FILES_BUCKET_NAME,
  283. - `${projectId0}/${fileId0}`
  284. - )
  285. + mockFilestore.deleteObject(projectId0, fileId0)
  286. const restoreFileAfter5s = async () => {
  287. await setTimeout(5_000)
  288. - await FILESTORE_PERSISTOR.sendStream(
  289. - USER_FILES_BUCKET_NAME,
  290. - `${projectId0}/${fileId0}`,
  291. - Stream.Readable.from([fileId0.toString()])
  292. - )
  293. + mockFilestore.addFile(projectId0, fileId0, fileId0.toString())
  294. }
  295. // use Promise.allSettled to ensure the above sendStream call finishes before this test completes
  296. const [
  297. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  298. index feb4612ddc23..5a590e347a94 100644
  299. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  300. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  301. @@ -178,7 +178,7 @@ const STREAM_HIGH_WATER_MARK = parseInt(
  302. const LOGGING_INTERVAL = parseInt(process.env.LOGGING_INTERVAL || '60000', 10)
  303. const SLEEP_BEFORE_EXIT = parseInt(process.env.SLEEP_BEFORE_EXIT || '1000', 10)
  304. -// Filestore endpoint location, the port is always hardcoded
  305. +// Filestore endpoint location
  306. const FILESTORE_HOST = process.env.FILESTORE_HOST || '127.0.0.1'
  307. const FILESTORE_PORT = process.env.FILESTORE_PORT || '3009'
  308. 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
  309. index 4e697b8bec2c..8f861d393451 100644
  310. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  311. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  312. @@ -27,7 +27,6 @@ import {
  313. } from '../../../../storage/lib/blob_store/index.js'
  314. import express from 'express'
  315. -import bodyParser from 'body-parser'
  316. chai.use(chaiExclude)
  317. const TIMEOUT = 20 * 1_000
  318. @@ -46,8 +45,6 @@ class MockFilestore {
  319. this.files = {}
  320. this.app = express()
  321. - this.app.use(bodyParser.json())
  322. - this.app.use(bodyParser.urlencoded({ extended: true }))
  323. this.app.get('/project/:projectId/file/:fileId', (req, res) => {
  324. const { projectId, fileId } = req.params