diff options
-rw-r--r-- | server/controllers/feeds.ts | 23 | ||||
-rw-r--r-- | server/initializers/constants.ts | 8 | ||||
-rw-r--r-- | server/lib/redis.ts | 57 | ||||
-rw-r--r-- | server/middlewares/cache.ts | 41 |
4 files changed, 113 insertions, 16 deletions
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 700c50ec8..3e384c48a 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -1,16 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { CONFIG } from '../initializers' | 2 | import { CONFIG, FEEDS } from '../initializers/constants' |
3 | import { | 3 | import { asyncMiddleware, feedsValidator, setDefaultSort, videosSortValidator } from '../middlewares' |
4 | asyncMiddleware, | ||
5 | feedsValidator, | ||
6 | setDefaultPagination, | ||
7 | setDefaultSort, | ||
8 | videosSortValidator | ||
9 | } from '../middlewares' | ||
10 | import { VideoModel } from '../models/video/video' | 4 | import { VideoModel } from '../models/video/video' |
11 | import * as Feed from 'pfeed' | 5 | import * as Feed from 'pfeed' |
12 | import { ResultList } from '../../shared/models' | 6 | import { ResultList } from '../../shared/models' |
13 | import { AccountModel } from '../models/account/account' | 7 | import { AccountModel } from '../models/account/account' |
8 | import { cacheRoute } from '../middlewares/cache' | ||
14 | 9 | ||
15 | const feedsRouter = express.Router() | 10 | const feedsRouter = express.Router() |
16 | 11 | ||
@@ -18,6 +13,7 @@ feedsRouter.get('/feeds/videos.:format', | |||
18 | videosSortValidator, | 13 | videosSortValidator, |
19 | setDefaultSort, | 14 | setDefaultSort, |
20 | asyncMiddleware(feedsValidator), | 15 | asyncMiddleware(feedsValidator), |
16 | asyncMiddleware(cacheRoute), | ||
21 | asyncMiddleware(generateFeed) | 17 | asyncMiddleware(generateFeed) |
22 | ) | 18 | ) |
23 | 19 | ||
@@ -31,8 +27,7 @@ export { | |||
31 | 27 | ||
32 | async function generateFeed (req: express.Request, res: express.Response, next: express.NextFunction) { | 28 | async function generateFeed (req: express.Request, res: express.Response, next: express.NextFunction) { |
33 | let feed = initFeed() | 29 | let feed = initFeed() |
34 | const paginationStart = 0 | 30 | const start = 0 |
35 | const paginationCount = 20 | ||
36 | 31 | ||
37 | let resultList: ResultList<VideoModel> | 32 | let resultList: ResultList<VideoModel> |
38 | const account: AccountModel = res.locals.account | 33 | const account: AccountModel = res.locals.account |
@@ -40,15 +35,15 @@ async function generateFeed (req: express.Request, res: express.Response, next: | |||
40 | if (account) { | 35 | if (account) { |
41 | resultList = await VideoModel.listAccountVideosForApi( | 36 | resultList = await VideoModel.listAccountVideosForApi( |
42 | account.id, | 37 | account.id, |
43 | paginationStart, | 38 | start, |
44 | paginationCount, | 39 | FEEDS.COUNT, |
45 | req.query.sort, | 40 | req.query.sort, |
46 | true | 41 | true |
47 | ) | 42 | ) |
48 | } else { | 43 | } else { |
49 | resultList = await VideoModel.listForApi( | 44 | resultList = await VideoModel.listForApi( |
50 | paginationStart, | 45 | start, |
51 | paginationCount, | 46 | FEEDS.COUNT, |
52 | req.query.sort, | 47 | req.query.sort, |
53 | req.query.filter, | 48 | req.query.filter, |
54 | true | 49 | true |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 56d39529e..9fde989c5 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -423,6 +423,13 @@ const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->' | |||
423 | 423 | ||
424 | // --------------------------------------------------------------------------- | 424 | // --------------------------------------------------------------------------- |
425 | 425 | ||
426 | const FEEDS = { | ||
427 | COUNT: 20, | ||
428 | CACHE_LIFETIME: 1000 * 60 * 15 // 15 minutes | ||
429 | } | ||
430 | |||
431 | // --------------------------------------------------------------------------- | ||
432 | |||
426 | // Special constants for a test instance | 433 | // Special constants for a test instance |
427 | if (isTestInstance() === true) { | 434 | if (isTestInstance() === true) { |
428 | ACTOR_FOLLOW_SCORE.BASE = 20 | 435 | ACTOR_FOLLOW_SCORE.BASE = 20 |
@@ -462,6 +469,7 @@ export { | |||
462 | SERVER_ACTOR_NAME, | 469 | SERVER_ACTOR_NAME, |
463 | PRIVATE_RSA_KEY_SIZE, | 470 | PRIVATE_RSA_KEY_SIZE, |
464 | SORTABLE_COLUMNS, | 471 | SORTABLE_COLUMNS, |
472 | FEEDS, | ||
465 | STATIC_MAX_AGE, | 473 | STATIC_MAX_AGE, |
466 | STATIC_PATHS, | 474 | STATIC_PATHS, |
467 | ACTIVITY_PUB, | 475 | ACTIVITY_PUB, |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index 41f4c9869..1e7c0a821 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -1,7 +1,14 @@ | |||
1 | import * as express from 'express' | ||
1 | import { createClient, RedisClient } from 'redis' | 2 | import { createClient, RedisClient } from 'redis' |
2 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
3 | import { generateRandomString } from '../helpers/utils' | 4 | import { generateRandomString } from '../helpers/utils' |
4 | import { CONFIG, USER_PASSWORD_RESET_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers' | 5 | import { CONFIG, FEEDS, USER_PASSWORD_RESET_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers' |
6 | |||
7 | type CachedRoute = { | ||
8 | body: string, | ||
9 | contentType?: string | ||
10 | statusCode?: string | ||
11 | } | ||
5 | 12 | ||
6 | class Redis { | 13 | class Redis { |
7 | 14 | ||
@@ -54,6 +61,22 @@ class Redis { | |||
54 | return this.exists(this.buildViewKey(ip, videoUUID)) | 61 | return this.exists(this.buildViewKey(ip, videoUUID)) |
55 | } | 62 | } |
56 | 63 | ||
64 | async getCachedRoute (req: express.Request) { | ||
65 | const cached = await this.getObject(this.buildCachedRouteKey(req)) | ||
66 | |||
67 | return cached as CachedRoute | ||
68 | } | ||
69 | |||
70 | setCachedRoute (req: express.Request, body: any, contentType?: string, statusCode?: number) { | ||
71 | const cached: CachedRoute = { | ||
72 | body: body.toString(), | ||
73 | contentType, | ||
74 | statusCode: statusCode.toString() | ||
75 | } | ||
76 | |||
77 | return this.setObject(this.buildCachedRouteKey(req), cached, FEEDS.CACHE_LIFETIME) | ||
78 | } | ||
79 | |||
57 | listJobs (jobsPrefix: string, state: string, mode: 'alpha', order: 'ASC' | 'DESC', offset: number, count: number) { | 80 | listJobs (jobsPrefix: string, state: string, mode: 'alpha', order: 'ASC' | 'DESC', offset: number, count: number) { |
58 | return new Promise<string[]>((res, rej) => { | 81 | return new Promise<string[]>((res, rej) => { |
59 | this.client.sort(jobsPrefix + ':jobs:' + state, 'by', mode, order, 'LIMIT', offset.toString(), count.toString(), (err, values) => { | 82 | this.client.sort(jobsPrefix + ':jobs:' + state, 'by', mode, order, 'LIMIT', offset.toString(), count.toString(), (err, values) => { |
@@ -79,13 +102,39 @@ class Redis { | |||
79 | this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => { | 102 | this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => { |
80 | if (err) return rej(err) | 103 | if (err) return rej(err) |
81 | 104 | ||
82 | if (ok !== 'OK') return rej(new Error('Redis result is not OK.')) | 105 | if (ok !== 'OK') return rej(new Error('Redis set result is not OK.')) |
83 | 106 | ||
84 | return res() | 107 | return res() |
85 | }) | 108 | }) |
86 | }) | 109 | }) |
87 | } | 110 | } |
88 | 111 | ||
112 | private setObject (key: string, obj: { [ id: string ]: string }, expirationMilliseconds: number) { | ||
113 | return new Promise<void>((res, rej) => { | ||
114 | this.client.hmset(this.prefix + key, obj, (err, ok) => { | ||
115 | if (err) return rej(err) | ||
116 | if (!ok) return rej(new Error('Redis mset result is not OK.')) | ||
117 | |||
118 | this.client.pexpire(this.prefix + key, expirationMilliseconds, (err, ok) => { | ||
119 | if (err) return rej(err) | ||
120 | if (!ok) return rej(new Error('Redis expiration result is not OK.')) | ||
121 | |||
122 | return res() | ||
123 | }) | ||
124 | }) | ||
125 | }) | ||
126 | } | ||
127 | |||
128 | private getObject (key: string) { | ||
129 | return new Promise<{ [ id: string ]: string }>((res, rej) => { | ||
130 | this.client.hgetall(this.prefix + key, (err, value) => { | ||
131 | if (err) return rej(err) | ||
132 | |||
133 | return res(value) | ||
134 | }) | ||
135 | }) | ||
136 | } | ||
137 | |||
89 | private exists (key: string) { | 138 | private exists (key: string) { |
90 | return new Promise<boolean>((res, rej) => { | 139 | return new Promise<boolean>((res, rej) => { |
91 | this.client.exists(this.prefix + key, (err, existsNumber) => { | 140 | this.client.exists(this.prefix + key, (err, existsNumber) => { |
@@ -104,6 +153,10 @@ class Redis { | |||
104 | return videoUUID + '-' + ip | 153 | return videoUUID + '-' + ip |
105 | } | 154 | } |
106 | 155 | ||
156 | private buildCachedRouteKey (req: express.Request) { | ||
157 | return req.method + '-' + req.originalUrl | ||
158 | } | ||
159 | |||
107 | static get Instance () { | 160 | static get Instance () { |
108 | return this.instance || (this.instance = new this()) | 161 | return this.instance || (this.instance = new this()) |
109 | } | 162 | } |
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts new file mode 100644 index 000000000..a2c7f7cbd --- /dev/null +++ b/server/middlewares/cache.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | import * as express from 'express' | ||
2 | import { Redis } from '../lib/redis' | ||
3 | import { logger } from '../helpers/logger' | ||
4 | |||
5 | async function cacheRoute (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
6 | const cached = await Redis.Instance.getCachedRoute(req) | ||
7 | |||
8 | // Not cached | ||
9 | if (!cached) { | ||
10 | logger.debug('Not cached result for route %s.', req.originalUrl) | ||
11 | |||
12 | const sendSave = res.send.bind(res) | ||
13 | |||
14 | res.send = (body) => { | ||
15 | if (res.statusCode >= 200 && res.statusCode < 400) { | ||
16 | Redis.Instance.setCachedRoute(req, body, res.getHeader('content-type').toString(), res.statusCode) | ||
17 | .catch(err => logger.error('Cannot cache route.', { err })) | ||
18 | } | ||
19 | |||
20 | return sendSave(body) | ||
21 | } | ||
22 | |||
23 | return next() | ||
24 | } | ||
25 | |||
26 | if (cached.contentType) res.contentType(cached.contentType) | ||
27 | |||
28 | if (cached.statusCode) { | ||
29 | const statusCode = parseInt(cached.statusCode, 10) | ||
30 | if (!isNaN(statusCode)) res.status(statusCode) | ||
31 | } | ||
32 | |||
33 | logger.debug('Use cached result for %s.', req.originalUrl) | ||
34 | return res.send(cached.body).end() | ||
35 | } | ||
36 | |||
37 | // --------------------------------------------------------------------------- | ||
38 | |||
39 | export { | ||
40 | cacheRoute | ||
41 | } | ||