From ecaf152b4a6eb702375debaef0dddc2cca798116 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 Apr 2023 17:32:09 +0900 Subject: [PATCH] enhance(backend): improve cache --- .../{UserCacheService.ts => CacheService.ts} | 17 +++- packages/backend/src/core/CoreModule.ts | 12 +-- .../backend/src/core/InstanceActorService.ts | 12 +-- .../backend/src/core/NoteCreateService.ts | 6 +- .../backend/src/core/NotificationService.ts | 18 ++-- packages/backend/src/core/RelayService.ts | 8 +- packages/backend/src/core/RoleService.ts | 30 +++--- .../core/activitypub/ApDbResolverService.ts | 10 +- .../activitypub/models/ApPersonService.ts | 14 +-- .../entities/NotificationEntityService.ts | 1 + .../src/core/entities/UserEntityService.ts | 1 - packages/backend/src/misc/cache.ts | 97 +++++++++++++++++-- .../processors/DeliverProcessorService.ts | 10 +- .../src/server/NodeinfoServerService.ts | 8 +- .../src/server/api/AuthenticateService.ts | 8 +- .../api/endpoints/i/regenerate-token.ts | 2 +- .../src/server/api/endpoints/i/update.ts | 8 +- .../src/server/api/endpoints/mute/create.ts | 3 + .../api/endpoints/renote-mute/create.ts | 2 - .../backend/src/server/api/stream/types.ts | 2 +- packages/backend/test/unit/RoleService.ts | 4 +- 21 files changed, 184 insertions(+), 89 deletions(-) rename packages/backend/src/core/{UserCacheService.ts => CacheService.ts} (79%) diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/CacheService.ts similarity index 79% rename from packages/backend/src/core/UserCacheService.ts rename to packages/backend/src/core/CacheService.ts index e452caf5d..887baeb2c 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; -import type { UsersRepository } from '@/models/index.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import type { UserProfile, UsersRepository } from '@/models/index.js'; +import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import type { LocalUser, User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -10,13 +10,18 @@ import { StreamMessages } from '@/server/api/stream/types.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() -export class UserCacheService implements OnApplicationShutdown { +export class CacheService implements OnApplicationShutdown { public userByIdCache: MemoryKVCache; public localUserByNativeTokenCache: MemoryKVCache; public localUserByIdCache: MemoryKVCache; public uriPersonCache: MemoryKVCache; + public userProfileCache: RedisKVCache; + public userMutingsCache: RedisKVCache; constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis, @@ -31,6 +36,8 @@ export class UserCacheService implements OnApplicationShutdown { this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); this.localUserByIdCache = new MemoryKVCache(Infinity); this.uriPersonCache = new MemoryKVCache(Infinity); + this.userProfileCache = new RedisKVCache(this.redisClient, 'userProfile', 1000 * 60 * 60 * 24, 1000 * 60); + this.userMutingsCache = new RedisKVCache(this.redisClient, 'userMutings', 1000 * 60 * 60 * 24, 1000 * 60); this.redisSubscriber.on('message', this.onMessage); } @@ -52,7 +59,7 @@ export class UserCacheService implements OnApplicationShutdown { } } if (this.userEntityService.isLocalUser(user)) { - this.localUserByNativeTokenCache.set(user.token, user); + this.localUserByNativeTokenCache.set(user.token!, user); this.localUserByIdCache.set(user.id, user); } break; @@ -77,7 +84,7 @@ export class UserCacheService implements OnApplicationShutdown { } @bindThis - public findById(userId: User['id']) { + public findUserById(userId: User['id']) { return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index d67e80fc1..5c867e6cf 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -38,7 +38,7 @@ import { S3Service } from './S3Service.js'; import { SignupService } from './SignupService.js'; import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; import { UserBlockingService } from './UserBlockingService.js'; -import { UserCacheService } from './UserCacheService.js'; +import { CacheService } from './CacheService.js'; import { UserFollowingService } from './UserFollowingService.js'; import { UserKeypairStoreService } from './UserKeypairStoreService.js'; import { UserListService } from './UserListService.js'; @@ -159,7 +159,7 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; -const $UserCacheService: Provider = { provide: 'UserCacheService', useExisting: UserCacheService }; +const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; @@ -282,7 +282,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SignupService, TwoFactorAuthenticationService, UserBlockingService, - UserCacheService, + CacheService, UserFollowingService, UserKeypairStoreService, UserListService, @@ -399,7 +399,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SignupService, $TwoFactorAuthenticationService, $UserBlockingService, - $UserCacheService, + $CacheService, $UserFollowingService, $UserKeypairStoreService, $UserListService, @@ -517,7 +517,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting SignupService, TwoFactorAuthenticationService, UserBlockingService, - UserCacheService, + CacheService, UserFollowingService, UserKeypairStoreService, UserListService, @@ -633,7 +633,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $SignupService, $TwoFactorAuthenticationService, $UserBlockingService, - $UserCacheService, + $CacheService, $UserFollowingService, $UserKeypairStoreService, $UserListService, diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index 049e27dec..898fb4ce8 100644 --- a/packages/backend/src/core/InstanceActorService.ts +++ b/packages/backend/src/core/InstanceActorService.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull } from 'typeorm'; import type { LocalUser } from '@/models/entities/User.js'; import type { UsersRepository } from '@/models/index.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import { MemoryCache } from '@/misc/cache.js'; import { DI } from '@/di-symbols.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { bindThis } from '@/decorators.js'; @@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const; @Injectable() export class InstanceActorService { - private cache: MemoryKVCache; + private cache: MemoryCache; constructor( @Inject(DI.usersRepository) @@ -19,12 +19,12 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new MemoryKVCache(Infinity); + this.cache = new MemoryCache(Infinity); } @bindThis public async getInstanceActor(): Promise { - const cached = this.cache.get(null); + const cached = this.cache.get(); if (cached) return cached; const user = await this.usersRepository.findOneBy({ @@ -33,11 +33,11 @@ export class InstanceActorService { }) as LocalUser | undefined; if (user) { - this.cache.set(null, user); + this.cache.set(user); return user; } else { const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser; - this.cache.set(null, created); + this.cache.set(created); return created; } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 552f24104..83290b310 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -20,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js import { checkWordMute } from '@/misc/check-word-mute.js'; import type { Channel } from '@/models/entities/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import { MemoryCache } from '@/misc/cache.js'; import type { UserProfile } from '@/models/entities/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -47,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; -const mutedWordsCache = new MemoryKVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new MemoryCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -473,7 +473,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.incNotesCountOfUser(user); // Word mute - mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({ + mutedWordsCache.fetch(() => this.userProfilesRepository.find({ where: { enableWordMute: true, }, diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 2a4dbba6a..9c179f931 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -3,7 +3,7 @@ import Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; +import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -12,6 +12,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { IdService } from '@/core/IdService.js'; +import { CacheService } from '@/core/CacheService.js'; @Injectable() export class NotificationService implements OnApplicationShutdown { @@ -35,6 +36,7 @@ export class NotificationService implements OnApplicationShutdown { private idService: IdService, private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, + private cacheService: CacheService, ) { } @@ -49,7 +51,6 @@ export class NotificationService implements OnApplicationShutdown { '+', '-', 'COUNT', 1); - console.log('latestNotificationIdsRes', latestNotificationIdsRes); const latestNotificationId = latestNotificationIdsRes[0]?.[0]; if (latestNotificationId == null) return; @@ -72,9 +73,8 @@ export class NotificationService implements OnApplicationShutdown { type: Notification['type'], data: Partial, ): Promise { - // TODO: Cache - const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); - const isMuted = profile?.mutingNotificationTypes.includes(type); + const profile = await this.cacheService.userProfileCache.fetch(notifieeId, () => this.userProfilesRepository.findOneByOrFail({ userId: notifieeId })); + const isMuted = profile.mutingNotificationTypes.includes(type); if (isMuted) return null; if (data.notifierId) { @@ -82,12 +82,8 @@ export class NotificationService implements OnApplicationShutdown { return null; } - // TODO: cache - const mutings = await this.mutingsRepository.findOneBy({ - muterId: notifieeId, - muteeId: data.notifierId, - }); - if (mutings) { + const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId, () => this.mutingsRepository.findBy({ muterId: notifieeId }).then(xs => xs.map(x => x.muteeId))); + if (mutings.includes(data.notifierId)) { return null; } } diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index be5a4d4b0..4df7fb3bf 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -3,7 +3,7 @@ import { IsNull } from 'typeorm'; import type { LocalUser, User } from '@/models/entities/User.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import { MemoryCache } from '@/misc/cache.js'; import type { Relay } from '@/models/entities/Relay.js'; import { QueueService } from '@/core/QueueService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; @@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const; @Injectable() export class RelayService { - private relaysCache: MemoryKVCache; + private relaysCache: MemoryCache; constructor( @Inject(DI.usersRepository) @@ -30,7 +30,7 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new MemoryKVCache(1000 * 60 * 10); + this.relaysCache = new MemoryCache(1000 * 60 * 10); } @bindThis @@ -109,7 +109,7 @@ export class RelayService { public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise { if (activity == null) return; - const relays = await this.relaysCache.fetch(null, () => this.relaysRepository.findBy({ + const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ status: 'accepted', })); if (relays.length === 0) return; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 678bcfc33..52e6292a1 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { In } from 'typeorm'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import { MemoryKVCache, MemoryCache } from '@/misc/cache.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; -import { UserCacheService } from '@/core/UserCacheService.js'; +import { CacheService } from '@/core/CacheService.js'; import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @@ -57,7 +57,7 @@ export const DEFAULT_POLICIES: RolePolicies = { @Injectable() export class RoleService implements OnApplicationShutdown { - private rolesCache: MemoryKVCache; + private rolesCache: MemoryCache; private roleAssignmentByUserIdCache: MemoryKVCache; public static AlreadyAssignedError = class extends Error {}; @@ -77,14 +77,14 @@ export class RoleService implements OnApplicationShutdown { private roleAssignmentsRepository: RoleAssignmentsRepository, private metaService: MetaService, - private userCacheService: UserCacheService, + private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, private idService: IdService, ) { //this.onMessage = this.onMessage.bind(this); - this.rolesCache = new MemoryKVCache(Infinity); + this.rolesCache = new MemoryCache(Infinity); this.roleAssignmentByUserIdCache = new MemoryKVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); @@ -98,7 +98,7 @@ export class RoleService implements OnApplicationShutdown { const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'roleCreated': { - const cached = this.rolesCache.get(null); + const cached = this.rolesCache.get(); if (cached) { cached.push({ ...body, @@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown { break; } case 'roleUpdated': { - const cached = this.rolesCache.get(null); + const cached = this.rolesCache.get(); if (cached) { const i = cached.findIndex(x => x.id === body.id); if (i > -1) { @@ -125,9 +125,9 @@ export class RoleService implements OnApplicationShutdown { break; } case 'roleDeleted': { - const cached = this.rolesCache.get(null); + const cached = this.rolesCache.get(); if (cached) { - this.rolesCache.set(null, cached.filter(x => x.id !== body.id)); + this.rolesCache.set(cached.filter(x => x.id !== body.id)); } break; } @@ -214,9 +214,9 @@ export class RoleService implements OnApplicationShutdown { // 期限切れのロールを除外 assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); const assignedRoleIds = assigns.map(x => x.roleId); - const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); - const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; + const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); return [...assignedRoles, ...matchedCondRoles]; } @@ -231,11 +231,11 @@ export class RoleService implements OnApplicationShutdown { // 期限切れのロールを除外 assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); const assignedRoleIds = assigns.map(x => x.roleId); - const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); if (badgeCondRoles.length > 0) { - const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; + const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; } else { @@ -301,7 +301,7 @@ export class RoleService implements OnApplicationShutdown { @bindThis public async getModeratorIds(includeAdmins = true): Promise { - const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)), @@ -321,7 +321,7 @@ export class RoleService implements OnApplicationShutdown { @bindThis public async getAdministratorIds(): Promise { - const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); + const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const administratorRoles = roles.filter(r => r.isAdministrator); const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ roleId: In(administratorRoles.map(r => r.id)), diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index dc0a865bb..4b032be89 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -5,7 +5,7 @@ import type { NotesRepository, UserPublickeysRepository, UsersRepository } from import type { Config } from '@/config.js'; import { MemoryKVCache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; -import { UserCacheService } from '@/core/UserCacheService.js'; +import { CacheService } from '@/core/CacheService.js'; import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; import { RemoteUser, User } from '@/models/entities/User.js'; @@ -47,7 +47,7 @@ export class ApDbResolverService { @Inject(DI.userPublickeysRepository) private userPublickeysRepository: UserPublickeysRepository, - private userCacheService: UserCacheService, + private cacheService: CacheService, private apPersonService: ApPersonService, ) { this.publicKeyCache = new MemoryKVCache(Infinity); @@ -107,11 +107,11 @@ export class ApDbResolverService { if (parsed.local) { if (parsed.type !== 'users') return null; - return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ + return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ id: parsed.id, }).then(x => x ?? undefined)) ?? null; } else { - return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ + return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ uri: parsed.uri, })); } @@ -138,7 +138,7 @@ export class ApDbResolverService { if (key == null) return null; return { - user: await this.userCacheService.findById(key.userId) as RemoteUser, + user: await this.cacheService.findUserById(key.userId) as RemoteUser, key, }; } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 41f7eafa4..67e907c27 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -8,7 +8,7 @@ import type { Config } from '@/config.js'; import type { RemoteUser } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js'; import { truncate } from '@/misc/truncate.js'; -import type { UserCacheService } from '@/core/UserCacheService.js'; +import type { CacheService } from '@/core/CacheService.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import type Logger from '@/logger.js'; @@ -54,7 +54,7 @@ export class ApPersonService implements OnModuleInit { private metaService: MetaService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; - private userCacheService: UserCacheService; + private cacheService: CacheService; private apResolverService: ApResolverService; private apNoteService: ApNoteService; private apImageService: ApImageService; @@ -97,7 +97,7 @@ export class ApPersonService implements OnModuleInit { //private metaService: MetaService, //private federatedInstanceService: FederatedInstanceService, //private fetchInstanceMetadataService: FetchInstanceMetadataService, - //private userCacheService: UserCacheService, + //private cacheService: CacheService, //private apResolverService: ApResolverService, //private apNoteService: ApNoteService, //private apImageService: ApImageService, @@ -118,7 +118,7 @@ export class ApPersonService implements OnModuleInit { this.metaService = this.moduleRef.get('MetaService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); - this.userCacheService = this.moduleRef.get('UserCacheService'); + this.cacheService = this.moduleRef.get('CacheService'); this.apResolverService = this.moduleRef.get('ApResolverService'); this.apNoteService = this.moduleRef.get('ApNoteService'); this.apImageService = this.moduleRef.get('ApImageService'); @@ -207,14 +207,14 @@ export class ApPersonService implements OnModuleInit { public async fetchPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); - const cached = this.userCacheService.uriPersonCache.get(uri); + const cached = this.cacheService.uriPersonCache.get(uri); if (cached) return cached; // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(this.config.url + '/')) { const id = uri.split('/').pop(); const u = await this.usersRepository.findOneBy({ id }); - if (u) this.userCacheService.uriPersonCache.set(uri, u); + if (u) this.cacheService.uriPersonCache.set(uri, u); return u; } @@ -222,7 +222,7 @@ export class ApPersonService implements OnModuleInit { const exist = await this.usersRepository.findOneBy({ uri }); if (exist) { - this.userCacheService.uriPersonCache.set(uri, exist); + this.cacheService.uriPersonCache.set(uri, exist); return exist; } //#endregion diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 7cffb8d56..6b9a9d332 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -54,6 +54,7 @@ export class NotificationEntityService implements OnModuleInit { public async pack( src: Notification, meId: User['id'], + // eslint-disable-next-line @typescript-eslint/ban-types options: { }, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 71aa2ee6d..e8474c7e0 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -255,7 +255,6 @@ export class UserEntityService implements OnModuleInit { '+', '-', 'COUNT', 1); - console.log('latestNotificationIdsRes', latestNotificationIdsRes); const latestNotificationId = latestNotificationIdsRes[0]?.[0]; return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId); diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index a805d1842..870dfd237 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,9 +1,94 @@ +import Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; +// redis通すとDateのインスタンスはstringに変換されるので +type Serialized = { + [K in keyof T]: + T[K] extends Date + ? string + : T[K] extends (Date | null) + ? (string | null) + : T[K] extends Record + ? Serialized + : T[K]; +}; + +export class RedisKVCache { + private redisClient: Redis.Redis; + private name: string; + private lifetime: number; + private memoryCache: MemoryKVCache; + + constructor(redisClient: RedisKVCache['redisClient'], name: RedisKVCache['name'], lifetime: RedisKVCache['lifetime'], memoryCacheLifetime: number) { + this.redisClient = redisClient; + this.name = name; + this.lifetime = lifetime; + this.memoryCache = new MemoryKVCache(memoryCacheLifetime); + } + + @bindThis + public async set(key: string, value: T): Promise { + this.memoryCache.set(key, value); + if (this.lifetime === Infinity) { + await this.redisClient.set( + `kvcache:${this.name}:${key}`, + JSON.stringify(value), + ); + } else { + await this.redisClient.set( + `kvcache:${this.name}:${key}`, + JSON.stringify(value), + 'ex', Math.round(this.lifetime / 1000), + ); + } + } + + @bindThis + public async get(key: string): Promise | T | undefined> { + const memoryCached = this.memoryCache.get(key); + if (memoryCached !== undefined) return memoryCached; + + const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`); + if (cached == null) return undefined; + return JSON.parse(cached); + } + + @bindThis + public async delete(key: string): Promise { + this.memoryCache.delete(key); + await this.redisClient.del(`kvcache:${this.name}:${key}`); + } + + /** + * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します + * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + */ + @bindThis + public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: Serialized | T) => boolean): Promise | T> { + const cachedValue = await this.get(key); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + this.set(key, value); + return value; + } +} + // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? export class MemoryKVCache { - public cache: Map; + public cache: Map; private lifetime: number; constructor(lifetime: MemoryKVCache['lifetime']) { @@ -12,7 +97,7 @@ export class MemoryKVCache { } @bindThis - public set(key: string | null, value: T): void { + public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), value, @@ -20,7 +105,7 @@ export class MemoryKVCache { } @bindThis - public get(key: string | null): T | undefined { + public get(key: string): T | undefined { const cached = this.cache.get(key); if (cached == null) return undefined; if ((Date.now() - cached.date) > this.lifetime) { @@ -31,7 +116,7 @@ export class MemoryKVCache { } @bindThis - public delete(key: string | null) { + public delete(key: string) { this.cache.delete(key); } @@ -40,7 +125,7 @@ export class MemoryKVCache { * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします */ @bindThis - public async fetch(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -65,7 +150,7 @@ export class MemoryKVCache { * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします */ @bindThis - public async fetchMaybe(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetchMaybe(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 71865c778..a9af22ad0 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import { MemoryCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; @@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js'; @Injectable() export class DeliverProcessorService { private logger: Logger; - private suspendedHostsCache: MemoryKVCache; + private suspendedHostsCache: MemoryCache; private latest: string | null; constructor( @@ -46,7 +46,7 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new MemoryKVCache(1000 * 60 * 60); + this.suspendedHostsCache = new MemoryCache(1000 * 60 * 60); } @bindThis @@ -60,14 +60,14 @@ export class DeliverProcessorService { } // isSuspendedなら中断 - let suspendedHosts = this.suspendedHostsCache.get(null); + let suspendedHosts = this.suspendedHostsCache.get(); if (suspendedHosts == null) { suspendedHosts = await this.instancesRepository.find({ where: { isSuspended: true, }, }); - this.suspendedHostsCache.set(null, suspendedHosts); + this.suspendedHostsCache.set(suspendedHosts); } if (suspendedHosts.map(x => x.host).includes(this.utilityService.toPuny(host))) { return 'skip (suspended)'; diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 3387bd53a..66c1faaac 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { MemoryKVCache } from '@/misc/cache.js'; +import { MemoryCache } from '@/misc/cache.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import NotesChart from '@/core/chart/charts/notes.js'; @@ -118,17 +118,17 @@ export class NodeinfoServerService { }; }; - const cache = new MemoryKVCache>>(1000 * 60 * 10); + const cache = new MemoryCache>>(1000 * 60 * 10); fastify.get(nodeinfo2_1path, async (request, reply) => { - const base = await cache.fetch(null, () => nodeinfo2()); + const base = await cache.fetch(() => nodeinfo2()); reply.header('Cache-Control', 'public, max-age=600'); return { version: '2.1', ...base }; }); fastify.get(nodeinfo2_0path, async (request, reply) => { - const base = await cache.fetch(null, () => nodeinfo2()); + const base = await cache.fetch(() => nodeinfo2()); delete (base as any).software.repository; diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index cd6bce9ef..6548c475b 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -5,7 +5,7 @@ import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; import { MemoryKVCache } from '@/misc/cache.js'; import type { App } from '@/models/entities/App.js'; -import { UserCacheService } from '@/core/UserCacheService.js'; +import { CacheService } from '@/core/CacheService.js'; import isNativeToken from '@/misc/is-native-token.js'; import { bindThis } from '@/decorators.js'; @@ -30,7 +30,7 @@ export class AuthenticateService { @Inject(DI.appsRepository) private appsRepository: AppsRepository, - private userCacheService: UserCacheService, + private cacheService: CacheService, ) { this.appCache = new MemoryKVCache(Infinity); } @@ -42,7 +42,7 @@ export class AuthenticateService { } if (isNativeToken(token)) { - const user = await this.userCacheService.localUserByNativeTokenCache.fetch(token, + const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, () => this.usersRepository.findOneBy({ token }) as Promise); if (user == null) { @@ -67,7 +67,7 @@ export class AuthenticateService { lastUsedAt: new Date(), }); - const user = await this.userCacheService.localUserByIdCache.fetch(accessToken.userId, + const user = await this.cacheService.localUserByIdCache.fetch(accessToken.userId, () => this.usersRepository.findOneBy({ id: accessToken.userId, }) as Promise); diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index f942f43cc..786e64374 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -34,7 +34,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { const freshUser = await this.usersRepository.findOneByOrFail({ id: me.id }); - const oldToken = freshUser.token; + const oldToken = freshUser.token!; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b1eaab390..46b16e9dc 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -18,6 +18,7 @@ import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -152,6 +153,7 @@ export default class extends Endpoint { private accountUpdateService: AccountUpdateService, private hashtagService: HashtagService, private roleService: RoleService, + private cacheService: CacheService, ) { super(meta, paramDef, async (ps, _user, token) => { const user = await this.usersRepository.findOneByOrFail({ id: _user.id }); @@ -276,9 +278,13 @@ export default class extends Endpoint { includeSecrets: isSecure, }); + const updatedProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + this.cacheService.userProfileCache.set(user.id, updatedProfile); + // Publish meUpdated event this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj); - this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', await this.userProfilesRepository.findOneByOrFail({ userId: user.id })); + this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', updatedProfile); // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 if (user.isLocked && ps.isLocked === false) { diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index 9099eea52..fd062e1ca 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -7,6 +7,7 @@ import type { Muting } from '@/models/entities/Muting.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { CacheService } from '@/core/CacheService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -65,6 +66,7 @@ export default class extends Endpoint { private globalEventService: GlobalEventService, private getterService: GetterService, private idService: IdService, + private cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -103,6 +105,7 @@ export default class extends Endpoint { muteeId: mutee.id, } as Muting); + this.cacheService.userMutingsCache.delete(muter.id); this.globalEventService.publishUserEvent(me.id, 'mute', mutee); }); } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 051a005b6..b28526961 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -92,8 +92,6 @@ export default class extends Endpoint { muterId: muter.id, muteeId: mutee.id, } as RenoteMuting); - - // publishUserEvent(user.id, 'mute', mutee); }); } } diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index b8f50e054..1e6e51e76 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -19,7 +19,7 @@ import type { EventEmitter } from 'events'; //#region Stream type-body definitions export interface InternalStreamTypes { userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; }; - userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; }; + userTokenRegenerated: { id: User['id']; oldToken: string; newToken: string; }; remoteUserUpdated: { id: User['id']; }; follow: { followerId: User['id']; followeeId: User['id']; }; unfollow: { followerId: User['id']; followeeId: User['id']; }; diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 6fe04274e..907f1f2ed 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -11,7 +11,7 @@ import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { genAid } from '@/misc/id/aid.js'; -import { UserCacheService } from '@/core/UserCacheService.js'; +import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { sleep } from '../utils.js'; @@ -65,7 +65,7 @@ describe('RoleService', () => { ], providers: [ RoleService, - UserCacheService, + CacheService, IdService, GlobalEventService, ],