MigrationPersistor.js 7.6 KB

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