MigrationPersistor.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. const AbstractPersistor = require('./AbstractPersistor')
  2. const Logger = require('@overleaf/logger')
  3. const Metrics = require('@overleaf/metrics')
  4. const Stream = require('node:stream')
  5. const { pipeline } = require('node:stream/promises')
  6. const { NotFoundError, WriteError } = require('./Errors')
  7. // Persistor that wraps two other persistors. Talks to the 'primary' by default,
  8. // but will fall back to an older persistor in the case of a not-found error.
  9. // If `Settings.fallback.copyOnMiss` is set, this will copy files from the fallback
  10. // to the primary, in the event that they are missing.
  11. //
  12. // It is unlikely that the bucket/location name will be the same on the fallback
  13. // as the primary. The bucket names should be overridden in `Settings.fallback.buckets`
  14. // e.g.
  15. // Settings.fallback.buckets = {
  16. // myBucketOnS3: 'myBucketOnGCS'
  17. // }
  18. module.exports = class MigrationPersistor extends AbstractPersistor {
  19. /**
  20. * @param {AbstractPersistor} primaryPersistor
  21. * @param {AbstractPersistor} fallbackPersistor
  22. * @param settings
  23. */
  24. constructor(primaryPersistor, fallbackPersistor, settings) {
  25. super()
  26. /**
  27. * @type {AbstractPersistor}
  28. */
  29. this.primaryPersistor = primaryPersistor
  30. /**
  31. * @type {AbstractPersistor}
  32. */
  33. this.fallbackPersistor = fallbackPersistor
  34. this.settings = settings
  35. }
  36. async sendFile(...args) {
  37. return await this.primaryPersistor.sendFile(...args)
  38. }
  39. async sendStream(...args) {
  40. return await this.primaryPersistor.sendStream(...args)
  41. }
  42. async getRedirectUrl(...args) {
  43. return await this.primaryPersistor.getRedirectUrl(...args)
  44. }
  45. async getObjectMd5Hash(...args) {
  46. return await this._runWithFallback('getObjectMd5Hash', ...args)
  47. }
  48. async checkIfObjectExists(...args) {
  49. return await this._runWithFallback('checkIfObjectExists', ...args)
  50. }
  51. async getObjectSize(...args) {
  52. return await this._runWithFallback('getObjectSize', ...args)
  53. }
  54. async directorySize(...args) {
  55. return await this._runWithFallback('directorySize', ...args)
  56. }
  57. async deleteObject(...args) {
  58. return await this._runOnBoth('deleteObject', ...args)
  59. }
  60. async deleteDirectory(...args) {
  61. return await this._runOnBoth('deleteDirectory', ...args)
  62. }
  63. async getObjectStream(bucket, key, opts = {}) {
  64. const shouldCopy = this.settings.copyOnMiss && !opts.start && !opts.end
  65. try {
  66. return await this.primaryPersistor.getObjectStream(bucket, key, opts)
  67. } catch (err) {
  68. if (err instanceof NotFoundError) {
  69. const fallbackBucket = this._getFallbackBucket(bucket)
  70. const fallbackStream = await this.fallbackPersistor.getObjectStream(
  71. fallbackBucket,
  72. key,
  73. opts
  74. )
  75. // tee the stream to the client, and as a copy to the primary (if necessary)
  76. // start listening on both straight away so that we don't consume bytes
  77. // in one place before the other
  78. const returnStream = new Stream.PassThrough()
  79. pipeline(fallbackStream, returnStream).catch(error => {
  80. Logger.warn({ error }, 'failed to copy object from fallback')
  81. })
  82. if (shouldCopy) {
  83. const copyStream = new Stream.PassThrough()
  84. pipeline(fallbackStream, copyStream).catch(error => {
  85. Logger.warn({ error }, 'failed to copy object from fallback')
  86. })
  87. this._copyStreamFromFallbackAndVerify(
  88. copyStream,
  89. fallbackBucket,
  90. bucket,
  91. key,
  92. key
  93. ).catch(error => {
  94. Logger.warn({ error }, 'failed to copy file from fallback')
  95. })
  96. }
  97. return returnStream
  98. }
  99. throw err
  100. }
  101. }
  102. async copyObject(bucket, sourceKey, destKey) {
  103. try {
  104. return await this.primaryPersistor.copyObject(bucket, sourceKey, destKey)
  105. } catch (err) {
  106. if (err instanceof NotFoundError) {
  107. const fallbackBucket = this._getFallbackBucket(bucket)
  108. const fallbackStream = await this.fallbackPersistor.getObjectStream(
  109. fallbackBucket,
  110. sourceKey,
  111. {}
  112. )
  113. const copyStream = new Stream.PassThrough()
  114. pipeline(fallbackStream, copyStream).catch(error => {
  115. Logger.warn({ error }, 'failed to copy object from fallback')
  116. })
  117. if (this.settings.copyOnMiss) {
  118. const missStream = new Stream.PassThrough()
  119. pipeline(fallbackStream, missStream).catch(error => {
  120. Logger.warn({ error }, 'failed to copy object from fallback')
  121. })
  122. // copy from sourceKey -> sourceKey
  123. this._copyStreamFromFallbackAndVerify(
  124. missStream,
  125. fallbackBucket,
  126. bucket,
  127. sourceKey,
  128. sourceKey
  129. ).catch(() => {
  130. // swallow errors, as this runs in the background and will log a warning
  131. })
  132. }
  133. // copy from sourceKey -> destKey
  134. return await this._copyStreamFromFallbackAndVerify(
  135. copyStream,
  136. fallbackBucket,
  137. bucket,
  138. sourceKey,
  139. destKey
  140. )
  141. }
  142. throw err
  143. }
  144. }
  145. async _copyStreamFromFallbackAndVerify(
  146. stream,
  147. sourceBucket,
  148. destBucket,
  149. sourceKey,
  150. destKey
  151. ) {
  152. try {
  153. let sourceMd5
  154. try {
  155. sourceMd5 = await this.fallbackPersistor.getObjectMd5Hash(
  156. sourceBucket,
  157. sourceKey
  158. )
  159. } catch (err) {
  160. Logger.warn(err, 'error getting md5 hash from fallback persistor')
  161. }
  162. await this.primaryPersistor.sendStream(destBucket, destKey, stream, {
  163. sourceMd5,
  164. })
  165. } catch (err) {
  166. const error = new WriteError(
  167. 'unable to copy file to destination persistor',
  168. {
  169. sourceBucket,
  170. destBucket,
  171. sourceKey,
  172. destKey,
  173. },
  174. err
  175. )
  176. Metrics.inc('fallback.copy.failure')
  177. try {
  178. await this.primaryPersistor.deleteObject(destBucket, destKey)
  179. } catch (err) {
  180. error.info.cleanupError = new WriteError(
  181. 'unable to clean up destination copy artifact',
  182. {
  183. destBucket,
  184. destKey,
  185. },
  186. err
  187. )
  188. }
  189. throw error
  190. }
  191. }
  192. _getFallbackBucket(bucket) {
  193. return (this.settings.buckets && this.settings.buckets[bucket]) || bucket
  194. }
  195. async _runOnBoth(methodName, bucket, ...moreArgs) {
  196. const fallbackBucket = this._getFallbackBucket(bucket)
  197. await Promise.all([
  198. this.primaryPersistor[methodName](bucket, ...moreArgs),
  199. this.fallbackPersistor[methodName](fallbackBucket, ...moreArgs),
  200. ])
  201. }
  202. /**
  203. * @param {keyof AbstractPersistor} methodName
  204. * @param bucket
  205. * @param key
  206. * @param moreArgs
  207. */
  208. async _runWithFallback(methodName, bucket, key, ...moreArgs) {
  209. try {
  210. return await this.primaryPersistor[methodName](bucket, key, ...moreArgs)
  211. } catch (err) {
  212. if (err instanceof NotFoundError) {
  213. const fallbackBucket = this._getFallbackBucket(bucket)
  214. if (this.settings.copyOnMiss) {
  215. const fallbackStream = await this.fallbackPersistor.getObjectStream(
  216. fallbackBucket,
  217. key,
  218. {}
  219. )
  220. // run in background
  221. this._copyStreamFromFallbackAndVerify(
  222. fallbackStream,
  223. fallbackBucket,
  224. bucket,
  225. key,
  226. key
  227. ).catch(err => {
  228. Logger.warn({ err }, 'failed to copy file from fallback')
  229. })
  230. }
  231. return await this.fallbackPersistor[methodName](
  232. fallbackBucket,
  233. key,
  234. ...moreArgs
  235. )
  236. }
  237. throw err
  238. }
  239. }
  240. }