diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index e5b911ed3..37f13b115 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,10 +1,12 @@ export class Cache { public cache: Map; private lifetime: number; + public fetcher: (key: string | null) => Promise; - constructor(lifetime: Cache['lifetime']) { + constructor(lifetime: number, fetcher: Cache['fetcher']) { this.cache = new Map(); this.lifetime = lifetime; + this.fetcher = fetcher; } public set(key: string | null, value: T): void { @@ -17,10 +19,13 @@ export class Cache { public get(key: string | null): T | undefined { const cached = this.cache.get(key); if (cached == null) return undefined; + + // discard if past the cache lifetime if ((Date.now() - cached.date) > this.lifetime) { this.cache.delete(key); return undefined; } + return cached.value; } @@ -29,52 +34,22 @@ export class Cache { } /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + * If the value is cached, it is returned. Otherwise the fetcher is + * run to get the value. If the fetcher returns undefined, it is + * returned but not cached. */ - public async fetch(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { - const cachedValue = this.get(key); - if (cachedValue !== undefined) { - if (validator) { - if (validator(cachedValue)) { - // Cache HIT - return cachedValue; - } - } else { - // Cache HIT - return cachedValue; - } - } + public async fetch(key: string | null): Promise { + const cached = this.get(key); + if (cached !== undefined) { + return cached; + } else { + const value = await this.fetcher(); - // Cache MISS - const value = await fetcher(); - this.set(key, value); - return value; - } + // don't cache undefined + if (value !== undefined) + this.set(key, value); - /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします - */ - public async fetchMaybe(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { - const cachedValue = this.get(key); - if (cachedValue !== undefined) { - if (validator) { - if (validator(cachedValue)) { - // Cache HIT - return cachedValue; - } - } else { - // Cache HIT - return cachedValue; - } + return value; } - - // Cache MISS - const value = await fetcher(); - if (value !== undefined) { - this.set(key, value); - } - return value; } } diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index aa9247b41..e563d749e 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -3,22 +3,26 @@ import { Note } from '@/models/entities/note.js'; import { User } from '@/models/entities/user.js'; import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js'; import * as Acct from '@/misc/acct.js'; +import { MINUTE } from '@/const.js'; import { getFullApAccount } from './convert-host.js'; import { Packed } from './schema.js'; import { Cache } from './cache.js'; -const blockingCache = new Cache(1000 * 60 * 5); +const blockingCache = new Cache( + 5 * MINUTE, + (blockerId) => Blockings.findBy({ blockerId }).then(res => res.map(x => x.blockeeId)), +); -// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている +// designation for users you follow, list users and groups is disabled for performance reasons /** - * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい + * either noteUserFollowers or antennaUserFollowing must be specified */ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise { if (note.visibility === 'specified') return false; - // アンテナ作成者がノート作成者にブロックされていたらスキップ - const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); + // skip if the antenna creator is blocked by the note author + const blockings = await blockingCache.fetch(noteUser.id); if (blockings.some(blocking => blocking === antenna.userId)) return false; if (note.visibility === 'followers') { diff --git a/packages/backend/src/misc/keypair-store.ts b/packages/backend/src/misc/keypair-store.ts index 9babf3ec5..910f96258 100644 --- a/packages/backend/src/misc/keypair-store.ts +++ b/packages/backend/src/misc/keypair-store.ts @@ -3,8 +3,11 @@ import { User } from '@/models/entities/user.js'; import { UserKeypair } from '@/models/entities/user-keypair.js'; import { Cache } from './cache.js'; -const cache = new Cache(Infinity); +const cache = new Cache( + Infinity, + (userId) => UserKeypairs.findOneByOrFail({ userId }), +); export async function getUserKeypair(userId: User['id']): Promise { - return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId })); + return await cache.fetch(userId); } diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index 9a1246d4d..ffdb4af50 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -4,11 +4,24 @@ import { Emojis } from '@/models/index.js'; import { Emoji } from '@/models/entities/emoji.js'; import { Note } from '@/models/entities/note.js'; import { query } from '@/prelude/url.js'; +import { HOUR } from '@/const.js'; import { Cache } from './cache.js'; import { isSelfHost, toPunyNullable } from './convert-host.js'; import { decodeReaction } from './reaction-lib.js'; -const cache = new Cache(1000 * 60 * 60 * 12); +/** + * composite cache key: `${host ?? ''}:${name}` + */ +const cache = new Cache( + 12 * HOUR, + async (key) => { + const [host, name] = key.split(':'); + return (await Emojis.findOneBy({ + name, + host: host || IsNull(), + })) || null; + }, +); /** * Information needed to attach in ActivityPub @@ -51,12 +64,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu const { name, host } = parseEmojiStr(emojiName, noteUserHost); if (name == null) return null; - const queryOrNull = async () => (await Emojis.findOneBy({ - name, - host: host ?? IsNull(), - })) || null; - - const emoji = await cache.fetch(`${name} ${host}`, queryOrNull); + const emoji = await cache.fetch(`${host ?? ''}:${name}`); if (emoji == null) return null; @@ -105,7 +113,10 @@ export function aggregateNoteEmojis(notes: Note[]) { * Query list of emojis in bulk and add them to the cache. */ export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise { - const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null); + const notCachedEmojis = emojis.filter(emoji => { + // check if the cache has this emoji + return cache.get(`${emoji.host ?? ''}:${emoji.name}`) == null; + }); // check if there even are any uncached emoji to handle if (notCachedEmojis.length === 0) return; @@ -127,7 +138,7 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null }).then(emojis => { // store all emojis into the cache emojis.forEach(emoji => { - cache.set(`${emoji.name} ${emoji.host}`, emoji); + cache.set(`${emoji.host ?? ''}:${emoji.name}`, emoji); }); }); } diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 169a1926d..ee842a4b5 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -6,13 +6,16 @@ import { Packed } from '@/misc/schema.js'; import { awaitAll, Promiseable } from '@/prelude/await-all.js'; import { populateEmojis } from '@/misc/populate-emojis.js'; import { getAntennas } from '@/misc/antenna-cache.js'; -import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; +import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js'; import { Cache } from '@/misc/cache.js'; import { db } from '@/db/postgre.js'; import { Instance } from '../entities/instance.js'; import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js'; -const userInstanceCache = new Cache(1000 * 60 * 60 * 3); +const userInstanceCache = new Cache( + 3 * HOUR, + (host) => Instances.findOneBy({ host }).then(x => x ?? undefined), +); type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; type IsMeAndIsUserDetailed = @@ -309,17 +312,15 @@ export const UserRepository = db.getRepository(User).extend({ isModerator: user.isModerator || falsy, isBot: user.isBot || falsy, isCat: user.isCat || falsy, - instance: user.host ? userInstanceCache.fetch(user.host, - () => Instances.findOneBy({ host: user.host! }), - v => v != null, - ).then(instance => instance ? { - name: instance.name, - softwareName: instance.softwareName, - softwareVersion: instance.softwareVersion, - iconUrl: instance.iconUrl, - faviconUrl: instance.faviconUrl, - themeColor: instance.themeColor, - } : undefined) : undefined, + instance: !user.host ? undefined : userInstanceCache.fetch(user.host) + .then(instance => !instance ? undefined : { + name: instance.name, + softwareName: instance.softwareName, + softwareVersion: instance.softwareVersion, + iconUrl: instance.iconUrl, + faviconUrl: instance.faviconUrl, + themeColor: instance.themeColor, + }), emojis: populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts index 24e361875..097747970 100644 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ b/packages/backend/src/remote/activitypub/db-resolver.ts @@ -10,8 +10,14 @@ import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; import { IObject, getApId } from './type.js'; import { resolvePerson } from './models/person.js'; -const publicKeyCache = new Cache(Infinity); -const publicKeyByUserIdCache = new Cache(Infinity); +const publicKeyCache = new Cache( + Infinity, + (keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined), +); +const publicKeyByUserIdCache = new Cache( + Infinity, + (userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined), +); export type UriParseResult = { /** wether the URI was generated by us */ @@ -99,13 +105,9 @@ export default class DbResolver { if (parsed.local) { if (parsed.type !== 'users') return null; - return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({ - id: parsed.id, - }).then(x => x ?? undefined)) ?? null; + return await userByIdCache.fetch(parsed.id) ?? null; } else { - return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ - uri: parsed.uri, - })); + return await uriPersonCache.fetch(parsed.uri) ?? null; } } @@ -116,20 +118,12 @@ export default class DbResolver { user: CacheableRemoteUser; key: UserPublickey; } | null> { - const key = await publicKeyCache.fetch(keyId, async () => { - const key = await UserPublickeys.findOneBy({ - keyId, - }); - - if (key == null) return null; - - return key; - }, key => key != null); + const key = await publicKeyCache.fetch(keyId); if (key == null) return null; return { - user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser, + user: await userByIdCache.fetch(key.userId) as CacheableRemoteUser, key, }; } @@ -145,7 +139,7 @@ export default class DbResolver { if (user == null) return null; - const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null); + const key = await publicKeyByUserIdCache.fetch(user.id); return { user, diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts index cfcdfcf36..f6d6a646a 100644 --- a/packages/backend/src/server/api/authenticate.ts +++ b/packages/backend/src/server/api/authenticate.ts @@ -6,7 +6,10 @@ import { App } from '@/models/entities/app.js'; import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js'; import isNativeToken from './common/is-native-token.js'; -const appCache = new Cache(Infinity); +const appCache = new Cache( + Infinity, + (id) => Apps.findOneByOrFail({ id }), +); export class AuthenticationError extends Error { constructor(message: string) { @@ -39,8 +42,7 @@ export default async (authorization: string | null | undefined, bodyToken: strin const token: string = maybeToken; if (isNativeToken(token)) { - const user = await localUserByNativeTokenCache.fetch(token, - () => Users.findOneBy({ token }) as Promise); + const user = await localUserByNativeTokenCache.fetch(token); if (user == null) { throw new AuthenticationError('unknown token'); @@ -64,17 +66,13 @@ export default async (authorization: string | null | undefined, bodyToken: strin lastUsedAt: new Date(), }); - const user = await userByIdCache.fetch(accessToken.userId, - () => Users.findOneBy({ - id: accessToken.userId, - }) as Promise); + const user = await userByIdCache.fetch(accessToken.userId); // can't authorize remote users if (!Users.isLocalUser(user)) return [null, null]; if (accessToken.appId) { - const app = await appCache.fetch(accessToken.appId, - () => Apps.findOneByOrFail({ id: accessToken.appId! })); + const app = await appCache.fetch(accessToken.appId); return [user, { id: accessToken.id, diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 2cae03241..e7dac0886 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -36,13 +36,22 @@ import { Cache } from '@/misc/cache.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { getActiveWebhooks } from '@/misc/webhook-cache.js'; import { IActivity } from '@/remote/activitypub/type.js'; +import { MINUTE } from '@/const.js'; import { updateHashtags } from '../update-hashtag.js'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { createNotification } from '../create-notification.js'; import { addNoteToAntenna } from '../add-note-to-antenna.js'; import { deliverToRelays } from '../relay.js'; -const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>( + 5 * MINUTE, + () => UserProfiles.find({ + where: { + enableWordMute: true, + }, + select: ['userId', 'mutedWords'], + }), +); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -257,12 +266,7 @@ export default async (user: { id: User['id']; username: User['username']; host: incNotesCountOfUser(user); // Word mute - mutedWordsCache.fetch(null, () => UserProfiles.find({ - where: { - enableWordMute: true, - }, - select: ['userId', 'mutedWords'], - })).then(us => { + mutedWordsCache.fetch(null).then(us => { for (const u of us) { checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { if (shouldMute) { diff --git a/packages/backend/src/services/register-or-fetch-instance-doc.ts b/packages/backend/src/services/register-or-fetch-instance-doc.ts index 75fea50f2..8c625ff21 100644 --- a/packages/backend/src/services/register-or-fetch-instance-doc.ts +++ b/packages/backend/src/services/register-or-fetch-instance-doc.ts @@ -3,29 +3,27 @@ import { Instances } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { toPuny } from '@/misc/convert-host.js'; import { Cache } from '@/misc/cache.js'; +import { HOUR } from '@/const.js'; -const cache = new Cache(1000 * 60 * 60); +const cache = new Cache( + HOUR, + (host) => Instances.findOneBy({ host }).then(x => x ?? undefined), +); export async function registerOrFetchInstanceDoc(idnHost: string): Promise { const host = toPuny(idnHost); - const cached = cache.get(host); + const cached = cache.fetch(host); if (cached) return cached; - const index = await Instances.findOneBy({ host }); + // apparently a new instance + const i = await Instances.insert({ + id: genId(), + host, + caughtAt: new Date(), + lastCommunicatedAt: new Date(), + }).then(x => Instances.findOneByOrFail(x.identifiers[0])); - if (index == null) { - const i = await Instances.insert({ - id: genId(), - host, - caughtAt: new Date(), - lastCommunicatedAt: new Date(), - }).then(x => Instances.findOneByOrFail(x.identifiers[0])); - - cache.set(host, i); - return i; - } else { - cache.set(host, index); - return index; - } + cache.set(host, i); + return i; } diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index a05645f09..36bf88b9c 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -8,11 +8,21 @@ import { Users, Relays } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { Cache } from '@/misc/cache.js'; import { Relay } from '@/models/entities/relay.js'; +import { MINUTE } from '@/const.js'; import { createSystemUser } from './create-system-user.js'; const ACTOR_USERNAME = 'relay.actor' as const; -const relaysCache = new Cache(1000 * 60 * 10); +/** + * There is only one cache key: null. + * A cache is only used here to have expiring storage. + */ +const relaysCache = new Cache( + 10 * MINUTE, + () => Relays.findBy({ + status: 'accepted', + }), +); export async function getRelayActor(): Promise { const user = await Users.findOneBy({ @@ -83,9 +93,7 @@ export async function relayRejected(id: string) { export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) { if (activity == null) return; - const relays = await relaysCache.fetch(null, () => Relays.findBy({ - status: 'accepted', - })); + const relays = await relaysCache.fetch(null); if (relays.length === 0) return; // TODO diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts index ef939d12b..f7f58dba5 100644 --- a/packages/backend/src/services/user-cache.ts +++ b/packages/backend/src/services/user-cache.ts @@ -3,9 +3,18 @@ import { Users } from '@/models/index.js'; import { Cache } from '@/misc/cache.js'; import { subscriber } from '@/db/redis.js'; -export const userByIdCache = new Cache(Infinity); -export const localUserByNativeTokenCache = new Cache(Infinity); -export const uriPersonCache = new Cache(Infinity); +export const userByIdCache = new Cache( + Infinity, + (id) => Users.findOneBy({ id }).then(x => x ?? undefined), +); +export const localUserByNativeTokenCache = new Cache( + Infinity, + (token) => Users.findOneBy({ token }).then(x => x ?? undefined), +); +export const uriPersonCache = new Cache( + Infinity, + (uri) => Users.findOneBy({ uri }).then(x => x ?? undefined), +); subscriber.on('message', async (_, data) => { const obj = JSON.parse(data);