Handle concurrent requests in cache middleware
authorChocobozzz <me@florianbigard.com>
Wed, 23 May 2018 08:03:26 +0000 (10:03 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 23 May 2018 08:03:26 +0000 (10:03 +0200)
package.json
server/lib/redis.ts
server/middlewares/cache.ts
yarn.lock

index bf69c4ce0e3605337e84fa5f0721d934ed5e0b9b..910de3f39463ebda3125d57b8280e43f007b352d 100644 (file)
@@ -69,6 +69,7 @@
   },
   "dependencies": {
     "async": "^2.0.0",
+    "async-lock": "^1.1.2",
     "async-lru": "^1.1.1",
     "bcrypt": "^2.0.1",
     "bittorrent-tracker": "^9.0.0",
   },
   "devDependencies": {
     "@types/async": "^2.0.40",
+    "@types/async-lock": "^1.1.0",
     "@types/bcrypt": "^2.0.0",
     "@types/body-parser": "^1.16.3",
     "@types/chai": "^4.0.4",
index 97ff3598b683cacd7510509baac08249070f3bf1..5bd55109c7b7c05065a9dfca1228b05752f15aff 100644 (file)
@@ -88,6 +88,18 @@ class Redis {
     })
   }
 
+  generateResetPasswordKey (userId: number) {
+    return 'reset-password-' + userId
+  }
+
+  buildViewKey (ip: string, videoUUID: string) {
+    return videoUUID + '-' + ip
+  }
+
+  buildCachedRouteKey (req: express.Request) {
+    return req.method + '-' + req.originalUrl
+  }
+
   private getValue (key: string) {
     return new Promise<string>((res, rej) => {
       this.client.get(this.prefix + key, (err, value) => {
@@ -146,18 +158,6 @@ class Redis {
     })
   }
 
-  private generateResetPasswordKey (userId: number) {
-    return 'reset-password-' + userId
-  }
-
-  private buildViewKey (ip: string, videoUUID: string) {
-    return videoUUID + '-' + ip
-  }
-
-  private buildCachedRouteKey (req: express.Request) {
-    return req.method + '-' + req.originalUrl
-  }
-
   static get Instance () {
     return this.instance || (this.instance = new this())
   }
index c589ef683c1b1d64e4b04d4f28bb3bb7380f5993..bf66596879f945f218a9f9e1a6a7b774edd73682 100644 (file)
@@ -1,39 +1,52 @@
 import * as express from 'express'
+import * as AsyncLock from 'async-lock'
 import { Redis } from '../lib/redis'
 import { logger } from '../helpers/logger'
 
+const lock = new AsyncLock({ timeout: 5000 })
+
 function cacheRoute (lifetime: number) {
   return async function (req: express.Request, res: express.Response, next: express.NextFunction) {
-    const cached = await Redis.Instance.getCachedRoute(req)
+    const redisKey = Redis.Instance.buildCachedRouteKey(req)
+
+    await lock.acquire(redisKey, async (done) => {
+      const cached = await Redis.Instance.getCachedRoute(req)
 
-    // Not cached
-    if (!cached) {
-      logger.debug('Not cached result for route %s.', req.originalUrl)
+      // Not cached
+      if (!cached) {
+        logger.debug('Not cached result for route %s.', req.originalUrl)
 
-      const sendSave = res.send.bind(res)
+        const sendSave = res.send.bind(res)
 
-      res.send = (body) => {
-        if (res.statusCode >= 200 && res.statusCode < 400) {
-          const contentType = res.getHeader('content-type').toString()
-          Redis.Instance.setCachedRoute(req, body, lifetime, contentType, res.statusCode)
-               .catch(err => logger.error('Cannot cache route.', { err }))
+        res.send = (body) => {
+          if (res.statusCode >= 200 && res.statusCode < 400) {
+            const contentType = res.getHeader('content-type').toString()
+            Redis.Instance.setCachedRoute(req, body, lifetime, contentType, res.statusCode)
+                 .then(() => done())
+                 .catch(err => {
+                   logger.error('Cannot cache route.', { err })
+                   return done(err)
+                 })
+          }
+
+          return sendSave(body)
         }
 
-        return sendSave(body)
+        return next()
       }
 
-      return next()
-    }
+      if (cached.contentType) res.contentType(cached.contentType)
 
-    if (cached.contentType) res.contentType(cached.contentType)
+      if (cached.statusCode) {
+        const statusCode = parseInt(cached.statusCode, 10)
+        if (!isNaN(statusCode)) res.status(statusCode)
+      }
 
-    if (cached.statusCode) {
-      const statusCode = parseInt(cached.statusCode, 10)
-      if (!isNaN(statusCode)) res.status(statusCode)
-    }
+      logger.debug('Use cached result for %s.', req.originalUrl)
+      res.send(cached.body).end()
 
-    logger.debug('Use cached result for %s.', req.originalUrl)
-    return res.send(cached.body).end()
+      return done()
+    })
   }
 }
 
index 49af4df030d42e5d21f4a662c45a8e2d86c0c2df..a8660dbab39aa2ff0a8ae3a577c7366dd262fce7 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     esutils "^2.0.2"
     js-tokens "^3.0.0"
 
+"@types/async-lock@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.0.tgz#002b1ebeebd382aff66b68bed70a74c7bdd06e3e"
+
 "@types/async@^2.0.40":
   version "2.0.49"
   resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.49.tgz#92e33d13f74c895cb9a7f38ba97db8431ed14bc0"
@@ -618,6 +622,10 @@ async-limiter@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
 
+async-lock@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.1.2.tgz#d552b3f8fe93018bf917efcf66d3154b9035282a"
+
 async-lru@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/async-lru/-/async-lru-1.1.1.tgz#3edbf7e96484d5c2dd852a8bf9794fc07f5e7274"