Handle concurrent requests in cache middleware

pull/592/head
Chocobozzz 2018-05-23 10:03:26 +02:00
parent e1a540b5fa
commit b40f057594
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
4 changed files with 55 additions and 32 deletions

View File

@ -69,6 +69,7 @@
}, },
"dependencies": { "dependencies": {
"async": "^2.0.0", "async": "^2.0.0",
"async-lock": "^1.1.2",
"async-lru": "^1.1.1", "async-lru": "^1.1.1",
"bcrypt": "^2.0.1", "bcrypt": "^2.0.1",
"bittorrent-tracker": "^9.0.0", "bittorrent-tracker": "^9.0.0",
@ -120,6 +121,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/async": "^2.0.40", "@types/async": "^2.0.40",
"@types/async-lock": "^1.1.0",
"@types/bcrypt": "^2.0.0", "@types/bcrypt": "^2.0.0",
"@types/body-parser": "^1.16.3", "@types/body-parser": "^1.16.3",
"@types/chai": "^4.0.4", "@types/chai": "^4.0.4",

View 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) { private getValue (key: string) {
return new Promise<string>((res, rej) => { return new Promise<string>((res, rej) => {
this.client.get(this.prefix + key, (err, value) => { 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 () { static get Instance () {
return this.instance || (this.instance = new this()) return this.instance || (this.instance = new this())
} }

View File

@ -1,39 +1,52 @@
import * as express from 'express' import * as express from 'express'
import * as AsyncLock from 'async-lock'
import { Redis } from '../lib/redis' import { Redis } from '../lib/redis'
import { logger } from '../helpers/logger' import { logger } from '../helpers/logger'
const lock = new AsyncLock({ timeout: 5000 })
function cacheRoute (lifetime: number) { function cacheRoute (lifetime: number) {
return async function (req: express.Request, res: express.Response, next: express.NextFunction) { 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)
// Not cached await lock.acquire(redisKey, async (done) => {
if (!cached) { const cached = await Redis.Instance.getCachedRoute(req)
logger.debug('Not cached result for route %s.', req.originalUrl)
const sendSave = res.send.bind(res) // Not cached
if (!cached) {
logger.debug('Not cached result for route %s.', req.originalUrl)
res.send = (body) => { const sendSave = res.send.bind(res)
if (res.statusCode >= 200 && res.statusCode < 400) {
const contentType = res.getHeader('content-type').toString() res.send = (body) => {
Redis.Instance.setCachedRoute(req, body, lifetime, contentType, res.statusCode) if (res.statusCode >= 200 && res.statusCode < 400) {
.catch(err => logger.error('Cannot cache route.', { err })) 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) { logger.debug('Use cached result for %s.', req.originalUrl)
const statusCode = parseInt(cached.statusCode, 10) res.send(cached.body).end()
if (!isNaN(statusCode)) res.status(statusCode)
}
logger.debug('Use cached result for %s.', req.originalUrl) return done()
return res.send(cached.body).end() })
} }
} }

View File

@ -16,6 +16,10 @@
esutils "^2.0.2" esutils "^2.0.2"
js-tokens "^3.0.0" 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": "@types/async@^2.0.40":
version "2.0.49" version "2.0.49"
resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.49.tgz#92e33d13f74c895cb9a7f38ba97db8431ed14bc0" 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" version "1.0.0"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" 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: async-lru@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/async-lru/-/async-lru-1.1.1.tgz#3edbf7e96484d5c2dd852a8bf9794fc07f5e7274" resolved "https://registry.yarnpkg.com/async-lru/-/async-lru-1.1.1.tgz#3edbf7e96484d5c2dd852a8bf9794fc07f5e7274"