Use a singleton for model cache
authorChocobozzz <me@florianbigard.com>
Tue, 4 Feb 2020 10:26:51 +0000 (11:26 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 4 Feb 2020 10:26:51 +0000 (11:26 +0100)
server/models/account/account.ts
server/models/activitypub/actor.ts
server/models/model-cache.ts [new file with mode: 0644]

index 0905a0fb2affe7a04a41afdacd7fe619c603712b..a0081f25995cfefc34b4d4cc5d442e925d428be6 100644 (file)
@@ -32,8 +32,9 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ
 import { AccountBlocklistModel } from './account-blocklist'
 import { ServerBlocklistModel } from '../server/server-blocklist'
 import { ActorFollowModel } from '../activitypub/actor-follow'
-import { MAccountActor, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models'
+import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models'
 import * as Bluebird from 'bluebird'
+import { ModelCache } from '@server/models/model-cache'
 
 export enum ScopeNames {
   SUMMARY = 'SUMMARY'
@@ -218,8 +219,6 @@ export class AccountModel extends Model<AccountModel> {
   })
   BlockedAccounts: AccountBlocklistModel[]
 
-  private static cache: { [ id: string ]: any } = {}
-
   @BeforeDestroy
   static async sendDeleteIfOwned (instance: AccountModel, options) {
     if (!instance.Actor) {
@@ -247,45 +246,43 @@ export class AccountModel extends Model<AccountModel> {
   }
 
   static loadLocalByName (name: string): Bluebird<MAccountDefault> {
-    // The server actor never change, so we can easily cache it
-    if (name === SERVER_ACTOR_NAME && AccountModel.cache[name]) {
-      return Bluebird.resolve(AccountModel.cache[name])
-    }
-
-    const query = {
-      where: {
-        [Op.or]: [
-          {
-            userId: {
-              [Op.ne]: null
+    const fun = () => {
+      const query = {
+        where: {
+          [Op.or]: [
+            {
+              userId: {
+                [Op.ne]: null
+              }
+            },
+            {
+              applicationId: {
+                [Op.ne]: null
+              }
             }
-          },
+          ]
+        },
+        include: [
           {
-            applicationId: {
-              [Op.ne]: null
+            model: ActorModel,
+            required: true,
+            where: {
+              preferredUsername: name
             }
           }
         ]
-      },
-      include: [
-        {
-          model: ActorModel,
-          required: true,
-          where: {
-            preferredUsername: name
-          }
-        }
-      ]
-    }
+      }
 
-    return AccountModel.findOne(query)
-      .then(account => {
-        if (name === SERVER_ACTOR_NAME) {
-          AccountModel.cache[name] = account
-        }
+      return AccountModel.findOne(query)
+    }
 
-        return account
-      })
+    return ModelCache.Instance.doCache({
+      cacheType: 'local-account-name',
+      key: name,
+      fun,
+      // The server actor never change, so we can easily cache it
+      whitelist: () => name === SERVER_ACTOR_NAME
+    })
   }
 
   static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> {
index 00e8dc9541614b04a6f154742d66cb39bac8ed56..9e8303a7b0c9c2633e0151b880335dffd139a976 100644 (file)
@@ -48,6 +48,7 @@ import {
 } from '../../typings/models'
 import * as Bluebird from 'bluebird'
 import { Op, Transaction, literal } from 'sequelize'
+import { ModelCache } from '@server/models/model-cache'
 
 enum ScopeNames {
   FULL = 'FULL'
@@ -276,9 +277,6 @@ export class ActorModel extends Model<ActorModel> {
   })
   VideoChannel: VideoChannelModel
 
-  private static localNameCache: { [ id: string ]: any } = {}
-  private static localUrlCache: { [ id: string ]: any } = {}
-
   static load (id: number): Bluebird<MActor> {
     return ActorModel.unscoped().findByPk(id)
   }
@@ -345,54 +343,50 @@ export class ActorModel extends Model<ActorModel> {
   }
 
   static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> {
-    // The server actor never change, so we can easily cache it
-    if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.localNameCache[preferredUsername]) {
-      return Bluebird.resolve(ActorModel.localNameCache[preferredUsername])
-    }
+    const fun = () => {
+      const query = {
+        where: {
+          preferredUsername,
+          serverId: null
+        },
+        transaction
+      }
 
-    const query = {
-      where: {
-        preferredUsername,
-        serverId: null
-      },
-      transaction
+      return ActorModel.scope(ScopeNames.FULL)
+                       .findOne(query)
     }
 
-    return ActorModel.scope(ScopeNames.FULL)
-                     .findOne(query)
-                     .then(actor => {
-                       if (preferredUsername === SERVER_ACTOR_NAME) {
-                         ActorModel.localNameCache[preferredUsername] = actor
-                       }
-
-                       return actor
-                     })
+    return ModelCache.Instance.doCache({
+      cacheType: 'local-actor-name',
+      key: preferredUsername,
+      // The server actor never change, so we can easily cache it
+      whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
+      fun
+    })
   }
 
   static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorUrl> {
-    // The server actor never change, so we can easily cache it
-    if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.localUrlCache[preferredUsername]) {
-      return Bluebird.resolve(ActorModel.localUrlCache[preferredUsername])
-    }
+    const fun = () => {
+      const query = {
+        attributes: [ 'url' ],
+        where: {
+          preferredUsername,
+          serverId: null
+        },
+        transaction
+      }
 
-    const query = {
-      attributes: [ 'url' ],
-      where: {
-        preferredUsername,
-        serverId: null
-      },
-      transaction
+      return ActorModel.unscoped()
+                       .findOne(query)
     }
 
-    return ActorModel.unscoped()
-                     .findOne(query)
-                     .then(actor => {
-                       if (preferredUsername === SERVER_ACTOR_NAME) {
-                         ActorModel.localUrlCache[preferredUsername] = actor
-                       }
-
-                       return actor
-                     })
+    return ModelCache.Instance.doCache({
+      cacheType: 'local-actor-name',
+      key: preferredUsername,
+      // The server actor never change, so we can easily cache it
+      whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
+      fun
+    })
   }
 
   static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
diff --git a/server/models/model-cache.ts b/server/models/model-cache.ts
new file mode 100644 (file)
index 0000000..bfa163b
--- /dev/null
@@ -0,0 +1,54 @@
+import { Model } from 'sequelize-typescript'
+import * as Bluebird from 'bluebird'
+import { logger } from '@server/helpers/logger'
+
+type ModelCacheType =
+  'local-account-name'
+  | 'local-actor-name'
+  | 'local-actor-url'
+
+class ModelCache {
+
+  private static instance: 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()
+  }
+
+  private constructor () {
+  }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+
+  doCache<T extends Model> (options: {
+    cacheType: ModelCacheType
+    key: string
+    fun: () => Bluebird<T>
+    whitelist?: () => boolean
+  }) {
+    const { cacheType, key, fun, whitelist } = options
+
+    if (whitelist && whitelist() !== true) return fun()
+
+    const cache = this.localCache[cacheType]
+
+    if (cache.has(key)) {
+      logger.debug('Model cache hit for %s -> %s.', cacheType, key)
+      return Bluebird.resolve<T>(cache.get(key))
+    }
+
+    return fun().then(m => {
+      if (!whitelist || whitelist()) cache.set(key, m)
+
+      return m
+    })
+  }
+}
+
+export {
+  ModelCache
+}