Add model cache for video
authorChocobozzz <me@florianbigard.com>
Tue, 4 Feb 2020 14:00:47 +0000 (15:00 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 4 Feb 2020 14:00:47 +0000 (15:00 +0100)
When fetching only immutable attributes

server/controllers/activitypub/client.ts
server/helpers/middlewares/videos.ts
server/helpers/video.ts
server/middlewares/validators/videos/videos.ts
server/models/model-cache.ts
server/models/video/video.ts
server/typings/express.ts
server/typings/models/video/video.ts

index 2812bfe1e8e9b194145330cc6080abff6db831da..9a5fd6084cc4b6ebeaf2af7712faec4d9205a566 100644 (file)
@@ -37,7 +37,7 @@ import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
 import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
 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()
 
@@ -85,7 +85,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
 )
 activityPubClientRouter.get('/videos/watch/:id/announces',
   executeIfActivityPub,
-  asyncMiddleware(videosCustomGetValidator('only-video')),
+  asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
   asyncMiddleware(videoAnnouncesController)
 )
 activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
@@ -95,17 +95,17 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
 )
 activityPubClientRouter.get('/videos/watch/:id/likes',
   executeIfActivityPub,
-  asyncMiddleware(videosCustomGetValidator('only-video')),
+  asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
   asyncMiddleware(videoLikesController)
 )
 activityPubClientRouter.get('/videos/watch/:id/dislikes',
   executeIfActivityPub,
-  asyncMiddleware(videosCustomGetValidator('only-video')),
+  asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
   asyncMiddleware(videoDislikesController)
 )
 activityPubClientRouter.get('/videos/watch/:id/comments',
   executeIfActivityPub,
-  asyncMiddleware(videosCustomGetValidator('only-video')),
+  asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
   asyncMiddleware(videoCommentsController)
 )
 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) {
-  const video = res.locals.onlyVideo
+  const video = res.locals.onlyImmutableVideo
 
   const handler = async (start: number, count: number) => {
     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) {
-  const video = res.locals.onlyVideo
+  const video = res.locals.onlyImmutableVideo
   const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
 
   return activityPubResponse(activityPubContextify(json), res)
 }
 
 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))
 
   return activityPubResponse(activityPubContextify(json), res)
 }
 
 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 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)
 }
 
-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 result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
     return {
index 74f529804e7abeb30494f064f1fdd59743d3c9a2..409f78650a63e24427c21e894b403320e46f4ee0 100644 (file)
@@ -2,7 +2,16 @@ import { Response } from 'express'
 import { fetchVideo, VideoFetchType } from '../video'
 import { UserRight } from '../../../shared/models/users'
 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') {
   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
       break
 
+    case 'only-immutable-attributes':
+      res.locals.onlyImmutableVideo = video as MVideoImmutable
+      break
+
     case 'id':
-      res.locals.videoId = video
+      res.locals.videoId = video as MVideoIdThumbnail
       break
 
     case 'only-video':
index 5b9c026b1e2b0357aaf2d0026551a24d8262dd8d..907564703b7f4a9097b216dd15ef39449da8ff97 100644 (file)
@@ -5,13 +5,15 @@ import {
   MVideoFullLight,
   MVideoIdThumbnail,
   MVideoThumbnail,
-  MVideoWithRights
+  MVideoWithRights,
+  MVideoImmutable
 } from '@server/typings/models'
 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: '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-with-rights', userId?: number): Bluebird<MVideoWithRights>
 function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
@@ -19,14 +21,16 @@ function fetchVideo (
   id: number | string,
   fetchType: VideoFetchType,
   userId?: number
-): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail>
+): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable>
 function fetchVideo (
   id: number | string,
   fetchType: VideoFetchType,
   userId?: number
-): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> {
+): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> {
   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') return VideoModel.load(id)
index 11dd02706570421382b27d121aac0cb92a056ada..c14184b35da7c4d3ebf94851445b52a380f5899d 100644 (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 [
     param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
 
index bfa163b6b5a3438086b58a1f23231fbe996b3876..8afe3834fb755c8d6e47324d7ba8423d601396a4 100644 (file)
@@ -6,6 +6,10 @@ type ModelCacheType =
   'local-account-name'
   | 'local-actor-name'
   | 'local-actor-url'
+  | 'video-immutable'
+
+type DeleteKey =
+  'video'
 
 class ModelCache {
 
@@ -14,7 +18,14 @@ class ModelCache {
   private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = {
     'local-account-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 () {
@@ -29,8 +40,9 @@ class ModelCache {
     key: string
     fun: () => Bluebird<T>
     whitelist?: () => boolean
+    deleteKey?: DeleteKey
   }) {
-    const { cacheType, key, fun, whitelist } = options
+    const { cacheType, key, fun, whitelist, deleteKey } = options
 
     if (whitelist && whitelist() !== true) return fun()
 
@@ -42,11 +54,34 @@ class ModelCache {
     }
 
     return fun().then(m => {
+      if (!m) return 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
     })
   }
+
+  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 {
index 1ec8d717e5d4c8592b0bec73c6f2844d6002e818..9e02d163f5f52ddf3b39f962ac7fdd8639d3800e 100644 (file)
@@ -120,7 +120,7 @@ import {
   MVideoFormattableDetails,
   MVideoForUser,
   MVideoFullLight,
-  MVideoIdThumbnail,
+  MVideoIdThumbnail, MVideoImmutable,
   MVideoThumbnail,
   MVideoThumbnailBlacklist,
   MVideoWithAllFiles,
@@ -132,6 +132,7 @@ import { MThumbnail } from '../../typings/models/video/thumbnail'
 import { VideoFile } from '@shared/models/videos/video-file.model'
 import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
 import validator from 'validator'
+import { ModelCache } from '@server/models/model-cache'
 
 export enum ScopeNames {
   AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -1074,6 +1075,11 @@ export class VideoModel extends Model<VideoModel> {
     return undefined
   }
 
+  @BeforeDestroy
+  static invalidateCache (instance: VideoModel) {
+    ModelCache.Instance.invalidateCache('video', instance.id)
+  }
+
   static listLocal (): Bluebird<MVideoWithAllFiles[]> {
     const query = {
       where: {
@@ -1468,6 +1474,28 @@ export class VideoModel extends Model<VideoModel> {
     ]).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> {
     const where = buildWhereIdOrUUID(id)
     const options = {
index 43a9b2c99026839df00ea0235cc22a4131c8ae03..f4188bf3daa0e45408e0df40aa12f5c35b49d856 100644 (file)
@@ -21,7 +21,7 @@ import {
 } from './models'
 import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
 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 { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
 import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
@@ -35,6 +35,7 @@ declare module 'express' {
 
     locals: {
       videoAll?: MVideoFullLight
+      onlyImmutableVideo?: MVideoImmutable
       onlyVideo?: MVideoThumbnail
       onlyVideoWithRights?: MVideoWithRights
       videoId?: MVideoIdThumbnail
index 7eff0a91320f305d9c1d1dedb73dd668c1a67904..3ebb5a76282247c18402e481c1fac37019ef8e07 100644 (file)
@@ -37,6 +37,7 @@ export type MVideoId = Pick<MVideo, 'id'>
 export type MVideoUrl = Pick<MVideo, 'url'>
 export type MVideoUUID = Pick<MVideo, 'uuid'>
 
+export type MVideoImmutable = Pick<MVideo, 'id' | 'url' | 'uuid'>
 export type MVideoIdUrl = MVideoId & MVideoUrl
 export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>