MigrationPersistor.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. await this.primaryPersistor.sendStream(destBucket, destKey, stream)
  154. } catch (err) {
  155. const error = new WriteError(
  156. 'unable to copy file to destination persistor',
  157. {
  158. sourceBucket,
  159. destBucket,
  160. sourceKey,
  161. destKey,
  162. },
  163. err
  164. )
  165. Metrics.inc('fallback.copy.failure')
  166. try {
  167. await this.primaryPersistor.deleteObject(destBucket, destKey)
  168. } catch (err) {
  169. error.info.cleanupError = new WriteError(
  170. 'unable to clean up destination copy artifact',
  171. {
  172. destBucket,
  173. destKey,
  174. },
  175. err
  176. )
  177. }
  178. throw error
  179. }
  180. }
  181. _getFallbackBucket(bucket) {
  182. return (this.settings.buckets && this.settings.buckets[bucket]) || bucket
  183. }
  184. async _runOnBoth(methodName, bucket, ...moreArgs) {
  185. const fallbackBucket = this._getFallbackBucket(bucket)
  186. await Promise.all([
  187. this.primaryPersistor[methodName](bucket, ...moreArgs),
  188. this.fallbackPersistor[methodName](fallbackBucket, ...moreArgs),
  189. ])
  190. }
  191. /**
  192. * @param {keyof AbstractPersistor} methodName
  193. * @param bucket
  194. * @param key
  195. * @param moreArgs
  196. */
  197. async _runWithFallback(methodName, bucket, key, ...moreArgs) {
  198. try {
  199. return await this.primaryPersistor[methodName](bucket, key, ...moreArgs)
  200. } catch (err) {
  201. if (err instanceof NotFoundError) {
  202. const fallbackBucket = this._getFallbackBucket(bucket)
  203. if (this.settings.copyOnMiss) {
  204. const fallbackStream = await this.fallbackPersistor.getObjectStream(
  205. fallbackBucket,
  206. key,
  207. {}
  208. )
  209. // run in background
  210. this._copyStreamFromFallbackAndVerify(
  211. fallbackStream,
  212. fallbackBucket,
  213. bucket,
  214. key,
  215. key
  216. ).catch(err => {
  217. Logger.warn({ err }, 'failed to copy file from fallback')
  218. })
  219. }
  220. return await this.fallbackPersistor[methodName](
  221. fallbackBucket,
  222. key,
  223. ...moreArgs
  224. )
  225. }
  226. throw err
  227. }
  228. }
  229. }