Add model cache for video

When fetching only immutable attributes
This commit is contained in:
Chocobozzz 2020-02-04 15:00:47 +01:00
parent e436baf0b0
commit 7eba5e1fa8
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
8 changed files with 106 additions and 21 deletions

View File

@ -37,7 +37,7 @@ import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
import { VideoPlaylistModel } from '../../models/video/video-playlist' import { VideoPlaylistModel } from '../../models/video/video-playlist'
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption } from '@server/typings/models' import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption, MVideoId } from '@server/typings/models'
const activityPubClientRouter = express.Router() const activityPubClientRouter = express.Router()
@ -85,7 +85,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
) )
activityPubClientRouter.get('/videos/watch/:id/announces', activityPubClientRouter.get('/videos/watch/:id/announces',
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(videosCustomGetValidator('only-video')), asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videoAnnouncesController) asyncMiddleware(videoAnnouncesController)
) )
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
@ -95,17 +95,17 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
) )
activityPubClientRouter.get('/videos/watch/:id/likes', activityPubClientRouter.get('/videos/watch/:id/likes',
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(videosCustomGetValidator('only-video')), asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videoLikesController) asyncMiddleware(videoLikesController)
) )
activityPubClientRouter.get('/videos/watch/:id/dislikes', activityPubClientRouter.get('/videos/watch/:id/dislikes',
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(videosCustomGetValidator('only-video')), asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videoDislikesController) asyncMiddleware(videoDislikesController)
) )
activityPubClientRouter.get('/videos/watch/:id/comments', activityPubClientRouter.get('/videos/watch/:id/comments',
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(videosCustomGetValidator('only-video')), asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videoCommentsController) asyncMiddleware(videoCommentsController)
) )
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId', activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
@ -238,7 +238,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
} }
async function videoAnnouncesController (req: express.Request, res: express.Response) { async function videoAnnouncesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo const video = res.locals.onlyImmutableVideo
const handler = async (start: number, count: number) => { const handler = async (start: number, count: number) => {
const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
@ -253,21 +253,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
} }
async function videoLikesController (req: express.Request, res: express.Response) { async function videoLikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo const video = res.locals.onlyImmutableVideo
const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
return activityPubResponse(activityPubContextify(json), res) return activityPubResponse(activityPubContextify(json), res)
} }
async function videoDislikesController (req: express.Request, res: express.Response) { async function videoDislikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo const video = res.locals.onlyImmutableVideo
const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
return activityPubResponse(activityPubContextify(json), res) return activityPubResponse(activityPubContextify(json), res)
} }
async function videoCommentsController (req: express.Request, res: express.Response) { async function videoCommentsController (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo const video = res.locals.onlyImmutableVideo
const handler = async (start: number, count: number) => { const handler = async (start: number, count: number) => {
const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count) const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
@ -386,7 +386,7 @@ async function actorPlaylists (req: express.Request, account: MAccountId) {
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
} }
function videoRates (req: express.Request, rateType: VideoRateType, video: MVideo, url: string) { function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) {
const handler = async (start: number, count: number) => { const handler = async (start: number, count: number) => {
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
return { return {

View File

@ -2,7 +2,16 @@ import { Response } from 'express'
import { fetchVideo, VideoFetchType } from '../video' import { fetchVideo, VideoFetchType } from '../video'
import { UserRight } from '../../../shared/models/users' import { UserRight } from '../../../shared/models/users'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import { MUser, MUserAccountId, MVideoAccountLight, MVideoFullLight, MVideoThumbnail, MVideoWithRights } from '@server/typings/models' import {
MUser,
MUserAccountId,
MVideoAccountLight,
MVideoFullLight,
MVideoIdThumbnail,
MVideoImmutable,
MVideoThumbnail,
MVideoWithRights
} from '@server/typings/models'
async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') { async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
@ -22,8 +31,12 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
res.locals.videoAll = video as MVideoFullLight res.locals.videoAll = video as MVideoFullLight
break break
case 'only-immutable-attributes':
res.locals.onlyImmutableVideo = video as MVideoImmutable
break
case 'id': case 'id':
res.locals.videoId = video res.locals.videoId = video as MVideoIdThumbnail
break break
case 'only-video': case 'only-video':

View File

@ -5,13 +5,15 @@ import {
MVideoFullLight, MVideoFullLight,
MVideoIdThumbnail, MVideoIdThumbnail,
MVideoThumbnail, MVideoThumbnail,
MVideoWithRights MVideoWithRights,
MVideoImmutable
} from '@server/typings/models' } from '@server/typings/models'
import { Response } from 'express' import { Response } from 'express'
type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes'
function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight> function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight>
function fetchVideo (id: number | string, fetchType: 'only-immutable-attributes'): Bluebird<MVideoImmutable>
function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail> function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail>
function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights> function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights>
function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail> function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
@ -19,14 +21,16 @@ function fetchVideo (
id: number | string, id: number | string,
fetchType: VideoFetchType, fetchType: VideoFetchType,
userId?: number userId?: number
): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> ): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable>
function fetchVideo ( function fetchVideo (
id: number | string, id: number | string,
fetchType: VideoFetchType, fetchType: VideoFetchType,
userId?: number userId?: number
): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> { ): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> {
if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
if (fetchType === 'only-video') return VideoModel.load(id) if (fetchType === 'only-video') return VideoModel.load(id)

View File

@ -147,7 +147,10 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
}) })
} }
const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights', authenticateInQuery = false) => { const videosCustomGetValidator = (
fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
authenticateInQuery = false
) => {
return [ return [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),

View File

@ -6,6 +6,10 @@ type ModelCacheType =
'local-account-name' 'local-account-name'
| 'local-actor-name' | 'local-actor-name'
| 'local-actor-url' | 'local-actor-url'
| 'video-immutable'
type DeleteKey =
'video'
class ModelCache { class ModelCache {
@ -14,7 +18,14 @@ class ModelCache {
private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = { private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = {
'local-account-name': new Map(), 'local-account-name': new Map(),
'local-actor-name': new Map(), 'local-actor-name': new Map(),
'local-actor-url': new Map() 'local-actor-url': new Map(),
'video-immutable': new Map()
}
private readonly deleteIds: {
[deleteKey in DeleteKey]: Map<number, { cacheType: ModelCacheType, key: string }[]>
} = {
video: new Map()
} }
private constructor () { private constructor () {
@ -29,8 +40,9 @@ class ModelCache {
key: string key: string
fun: () => Bluebird<T> fun: () => Bluebird<T>
whitelist?: () => boolean whitelist?: () => boolean
deleteKey?: DeleteKey
}) { }) {
const { cacheType, key, fun, whitelist } = options const { cacheType, key, fun, whitelist, deleteKey } = options
if (whitelist && whitelist() !== true) return fun() if (whitelist && whitelist() !== true) return fun()
@ -42,11 +54,34 @@ class ModelCache {
} }
return fun().then(m => { return fun().then(m => {
if (!m) return m
if (!whitelist || whitelist()) cache.set(key, m) if (!whitelist || whitelist()) cache.set(key, m)
if (deleteKey) {
const map = this.deleteIds[deleteKey]
if (!map.has(m.id)) map.set(m.id, [])
const a = map.get(m.id)
a.push({ cacheType, key })
}
return m return m
}) })
} }
invalidateCache (deleteKey: DeleteKey, modelId: number) {
const map = this.deleteIds[deleteKey]
if (!map.has(modelId)) return
for (const toDelete of map.get(modelId)) {
logger.debug('Removing %s -> %d of model cache %s -> %s.', deleteKey, modelId, toDelete.cacheType, toDelete.key)
this.localCache[toDelete.cacheType].delete(toDelete.key)
}
map.delete(modelId)
}
} }
export { export {

View File

@ -120,7 +120,7 @@ import {
MVideoFormattableDetails, MVideoFormattableDetails,
MVideoForUser, MVideoForUser,
MVideoFullLight, MVideoFullLight,
MVideoIdThumbnail, MVideoIdThumbnail, MVideoImmutable,
MVideoThumbnail, MVideoThumbnail,
MVideoThumbnailBlacklist, MVideoThumbnailBlacklist,
MVideoWithAllFiles, MVideoWithAllFiles,
@ -132,6 +132,7 @@ import { MThumbnail } from '../../typings/models/video/thumbnail'
import { VideoFile } from '@shared/models/videos/video-file.model' import { VideoFile } from '@shared/models/videos/video-file.model'
import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
import validator from 'validator' import validator from 'validator'
import { ModelCache } from '@server/models/model-cache'
export enum ScopeNames { export enum ScopeNames {
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@ -1074,6 +1075,11 @@ export class VideoModel extends Model<VideoModel> {
return undefined return undefined
} }
@BeforeDestroy
static invalidateCache (instance: VideoModel) {
ModelCache.Instance.invalidateCache('video', instance.id)
}
static listLocal (): Bluebird<MVideoWithAllFiles[]> { static listLocal (): Bluebird<MVideoWithAllFiles[]> {
const query = { const query = {
where: { where: {
@ -1468,6 +1474,28 @@ export class VideoModel extends Model<VideoModel> {
]).findOne(options) ]).findOne(options)
} }
static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
const fun = () => {
const where = buildWhereIdOrUUID(id)
const options = {
attributes: [
'id', 'url', 'uuid'
],
where,
transaction: t
}
return VideoModel.unscoped().findOne(options)
}
return ModelCache.Instance.doCache({
cacheType: 'video-immutable',
key: '' + id,
deleteKey: 'video',
fun
})
}
static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
const where = buildWhereIdOrUUID(id) const where = buildWhereIdOrUUID(id)
const options = { const options = {

View File

@ -21,7 +21,7 @@ import {
} from './models' } from './models'
import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist' import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
import { MVideoImportDefault } from '@server/typings/models/video/video-import' import { MVideoImportDefault } from '@server/typings/models/video/video-import'
import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile } from '@server/typings/models' import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile, MVideoImmutable } from '@server/typings/models'
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element' import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element'
import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate' import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership' import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
@ -35,6 +35,7 @@ declare module 'express' {
locals: { locals: {
videoAll?: MVideoFullLight videoAll?: MVideoFullLight
onlyImmutableVideo?: MVideoImmutable
onlyVideo?: MVideoThumbnail onlyVideo?: MVideoThumbnail
onlyVideoWithRights?: MVideoWithRights onlyVideoWithRights?: MVideoWithRights
videoId?: MVideoIdThumbnail videoId?: MVideoIdThumbnail

View File

@ -37,6 +37,7 @@ export type MVideoId = Pick<MVideo, 'id'>
export type MVideoUrl = Pick<MVideo, 'url'> export type MVideoUrl = Pick<MVideo, 'url'>
export type MVideoUUID = Pick<MVideo, 'uuid'> export type MVideoUUID = Pick<MVideo, 'uuid'>
export type MVideoImmutable = Pick<MVideo, 'id' | 'url' | 'uuid'>
export type MVideoIdUrl = MVideoId & MVideoUrl export type MVideoIdUrl = MVideoId & MVideoUrl
export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'> export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>