From 30d699268450af375dabc2226ec4f3196a53f7f7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 Apr 2023 14:06:57 +0900 Subject: perf(backend): 通知をRedisに保存するように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #10168 --- packages/backend/src/server/api/EndpointsModule.ts | 4 - packages/backend/src/server/api/endpoints.ts | 2 - .../src/server/api/endpoints/admin/suspend-user.ts | 16 +-- .../src/server/api/endpoints/i/notifications.ts | 108 ++++++--------------- .../endpoints/notifications/mark-all-as-read.ts | 22 +---- .../src/server/api/endpoints/notifications/read.ts | 57 ----------- packages/backend/src/server/api/stream/index.ts | 3 +- 7 files changed, 37 insertions(+), 175 deletions(-) delete mode 100644 packages/backend/src/server/api/endpoints/notifications/read.ts (limited to 'packages/backend/src/server') diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index f39643abeb..cab2477414 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -268,7 +268,6 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; -import * as ep___notifications_read from './endpoints/notifications/read.js'; import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_delete from './endpoints/pages/delete.js'; @@ -600,7 +599,6 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__ const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; -const $notifications_read: Provider = { provide: 'ep:notifications/read', useClass: ep___notifications_read.default }; const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default }; const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default }; @@ -936,7 +934,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_userListTimeline, $notifications_create, $notifications_markAllAsRead, - $notifications_read, $pagePush, $pages_create, $pages_delete, @@ -1266,7 +1263,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_userListTimeline, $notifications_create, $notifications_markAllAsRead, - $notifications_read, $pagePush, $pages_create, $pages_delete, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 16b20c1a4d..e33c2349cd 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -268,7 +268,6 @@ import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; -import * as ep___notifications_read from './endpoints/notifications/read.js'; import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pages_create from './endpoints/pages/create.js'; import * as ep___pages_delete from './endpoints/pages/delete.js'; @@ -598,7 +597,6 @@ const eps = [ ['notes/user-list-timeline', ep___notes_userListTimeline], ['notifications/create', ep___notifications_create], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead], - ['notifications/read', ep___notifications_read], ['page-push', ep___pagePush], ['pages/create', ep___pages_create], ['pages/delete', ep___pages_delete], diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 3ad6c7c484..770b61850a 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -36,9 +36,6 @@ export default class extends Endpoint { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - @Inject(DI.notificationsRepository) - private notificationsRepository: NotificationsRepository, - private userEntityService: UserEntityService, private userFollowingService: UserFollowingService, private userSuspendService: UserSuspendService, @@ -73,7 +70,6 @@ export default class extends Endpoint { (async () => { await this.userSuspendService.doPostSuspend(user).catch(e => {}); await this.unFollowAll(user).catch(e => {}); - await this.readAllNotify(user).catch(e => {}); })(); }); } @@ -96,14 +92,4 @@ export default class extends Endpoint { await this.userFollowingService.unfollow(follower, followee, true); } } - - @bindThis - private async readAllNotify(notifier: User) { - await this.notificationsRepository.update({ - notifierId: notifier.id, - isRead: false, - }, { - isRead: true, - }); - } } diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index e3897d38bd..f27b4e86d4 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -1,6 +1,7 @@ -import { Brackets } from 'typeorm'; +import { Brackets, In } from 'typeorm'; +import Redis from 'ioredis'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js'; +import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js'; import { obsoleteNotificationTypes, notificationTypes } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; @@ -8,6 +9,8 @@ import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; +import { Notification } from '@/models/entities/Notification.js'; export const meta = { tags: ['account', 'notifications'], @@ -38,8 +41,6 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, - following: { type: 'boolean', default: false }, - unreadOnly: { type: 'boolean', default: false }, markAsRead: { type: 'boolean', default: true }, // 後方互換のため、廃止された通知タイプも受け付ける includeTypes: { type: 'array', items: { @@ -56,21 +57,22 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.notificationsRepository) - private notificationsRepository: NotificationsRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private idService: IdService, private notificationEntityService: NotificationEntityService, private notificationService: NotificationService, private queryService: QueryService, @@ -89,85 +91,39 @@ export default class extends Endpoint { const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; - const followingQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') - .select('muting.muteeId') - .where('muting.muterId = :muterId', { muterId: me.id }); - - const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile') - .select('user_profile.mutedInstances') - .where('user_profile.userId = :muterId', { muterId: me.id }); - - const suspendedQuery = this.usersRepository.createQueryBuilder('users') - .select('users.id') - .where('users.isSuspended = TRUE'); - - const query = this.queryService.makePaginationQuery(this.notificationsRepository.createQueryBuilder('notification'), ps.sinceId, ps.untilId) - .andWhere('notification.notifieeId = :meId', { meId: me.id }) - .leftJoinAndSelect('notification.notifier', 'notifier') - .leftJoinAndSelect('notification.note', 'note') - .leftJoinAndSelect('notifier.avatar', 'notifierAvatar') - .leftJoinAndSelect('notifier.banner', 'notifierBanner') - .leftJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('user.avatar', 'avatar') - .leftJoinAndSelect('user.banner', 'banner') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') - .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') - .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner'); - - // muted users - query.andWhere(new Brackets(qb => { qb - .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`) - .orWhere('notification.notifierId IS NULL'); - })); - query.setParameters(mutingQuery.getParameters()); - - // muted instances - query.andWhere(new Brackets(qb => { qb - .andWhere('notifier.host IS NULL') - .orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`); - })); - query.setParameters(mutingInstanceQuery.getParameters()); - - // suspended users - query.andWhere(new Brackets(qb => { qb - .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`) - .orWhere('notification.notifierId IS NULL'); - })); - - if (ps.following) { - query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id }); - query.setParameters(followingQuery.getParameters()); + const notificationsRes = await this.redisClient.xrevrange( + `notificationTimeline:${me.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', + '-', + 'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1 + + if (notificationsRes.length === 0) { + return []; } + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[]; + if (includeTypes && includeTypes.length > 0) { - query.andWhere('notification.type IN (:...includeTypes)', { includeTypes }); + notifications = notifications.filter(notification => includeTypes.includes(notification.type)); } else if (excludeTypes && excludeTypes.length > 0) { - query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes }); + notifications = notifications.filter(notification => !excludeTypes.includes(notification.type)); } - if (ps.unreadOnly) { - query.andWhere('notification.isRead = false'); + if (notifications.length === 0) { + return []; } - const notifications = await query.take(ps.limit).getMany(); - // Mark all as read - if (notifications.length > 0 && ps.markAsRead) { - this.notificationService.readNotification(me.id, notifications.map(x => x.id)); + if (ps.markAsRead) { + this.notificationService.readAllNotification(me.id); } - const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!); + const noteIds = notifications + .filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)) + .map(notification => notification.noteId!); - if (notes.length > 0) { + if (noteIds.length > 0) { + const notes = await this.notesRepository.findBy({ id: In(noteIds) }); this.noteReadService.read(me.id, notes); } diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index 09134cf48f..9ba6079189 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,9 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { NotificationsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; import { DI } from '@/di-symbols.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const meta = { tags: ['notifications', 'account'], @@ -23,24 +21,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( - @Inject(DI.notificationsRepository) - private notificationsRepository: NotificationsRepository, - - private globalEventService: GlobalEventService, - private pushNotificationService: PushNotificationService, + private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { - // Update documents - await this.notificationsRepository.update({ - notifieeId: me.id, - isRead: false, - }, { - isRead: true, - }); - - // 全ての通知を読みましたよというイベントを発行 - this.globalEventService.publishMainStream(me.id, 'readAllNotifications'); - this.pushNotificationService.pushNotification(me.id, 'readAllNotifications', undefined); + this.notificationService.readAllNotification(me.id); }); } } diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts deleted file mode 100644 index 6262c47fd0..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NotificationService } from '@/core/NotificationService.js'; - -export const meta = { - tags: ['notifications', 'account'], - - requireCredential: true, - - kind: 'write:notifications', - - description: 'Mark a notification as read.', - - errors: { - noSuchNotification: { - message: 'No such notification.', - code: 'NO_SUCH_NOTIFICATION', - id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e', - }, - }, -} as const; - -export const paramDef = { - oneOf: [ - { - type: 'object', - properties: { - notificationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['notificationId'], - }, - { - type: 'object', - properties: { - notificationIds: { - type: 'array', - items: { type: 'string', format: 'misskey:id' }, - maxItems: 100, - }, - }, - required: ['notificationIds'], - }, - ], -} as const; - -// eslint-disable-next-line import/no-default-export -@Injectable() -export default class extends Endpoint { - constructor( - private notificationService: NotificationService, - ) { - super(meta, paramDef, async (ps, me) => { - if ('notificationId' in ps) return this.notificationService.readNotification(me.id, [ps.notificationId]); - return this.notificationService.readNotification(me.id, ps.notificationIds); - }); - } -} diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index 7c6eb9a20a..f1f8bfd3a2 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -195,8 +195,7 @@ export default class Connection { @bindThis private onReadNotification(payload: any) { - if (!payload.id) return; - this.notificationService.readNotification(this.user!.id, [payload.id]); + this.notificationService.readAllNotification(this.user!.id); } /** -- cgit v1.2.3-freya From 7f3afac0a21b0a1465cb6c4ac19be608dcdbfe1d Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 Apr 2023 15:56:47 +0900 Subject: refactor(backend): rename Cache -> MemoryCache --- packages/backend/src/core/CustomEmojiService.ts | 6 +++--- packages/backend/src/core/FederatedInstanceService.ts | 6 +++--- packages/backend/src/core/InstanceActorService.ts | 6 +++--- packages/backend/src/core/NoteCreateService.ts | 4 ++-- packages/backend/src/core/RelayService.ts | 6 +++--- packages/backend/src/core/RoleService.ts | 10 +++++----- packages/backend/src/core/UserBlockingService.ts | 6 +++--- packages/backend/src/core/UserCacheService.ts | 18 +++++++++--------- packages/backend/src/core/UserKeypairStoreService.ts | 6 +++--- .../src/core/activitypub/ApDbResolverService.ts | 10 +++++----- .../backend/src/core/entities/UserEntityService.ts | 6 +++--- packages/backend/src/misc/cache.ts | 8 ++++---- .../src/queue/processors/DeliverProcessorService.ts | 6 +++--- packages/backend/src/server/NodeinfoServerService.ts | 4 ++-- packages/backend/src/server/api/AuthenticateService.ts | 6 +++--- 15 files changed, 54 insertions(+), 54 deletions(-) (limited to 'packages/backend/src/server') diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index a62854c61c..1c3b60e5d7 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -8,7 +8,7 @@ import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; import type { EmojisRepository, Note } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import type { Config } from '@/config.js'; import { ReactionService } from '@/core/ReactionService.js'; @@ -16,7 +16,7 @@ import { query } from '@/misc/prelude/url.js'; @Injectable() export class CustomEmojiService { - private cache: KVCache; + private cache: MemoryKVCache; constructor( @Inject(DI.config) @@ -34,7 +34,7 @@ export class CustomEmojiService { private globalEventService: GlobalEventService, private reactionService: ReactionService, ) { - this.cache = new KVCache(1000 * 60 * 60 * 12); + this.cache = new MemoryKVCache(1000 * 60 * 60 * 12); } @bindThis diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index b85791e43f..2c6d3ac508 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { InstancesRepository } from '@/models/index.js'; import type { Instance } from '@/models/entities/Instance.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js'; @Injectable() export class FederatedInstanceService { - private cache: KVCache; + private cache: MemoryKVCache; constructor( @Inject(DI.instancesRepository) @@ -18,7 +18,7 @@ export class FederatedInstanceService { private utilityService: UtilityService, private idService: IdService, ) { - this.cache = new KVCache(1000 * 60 * 60); + this.cache = new MemoryKVCache(1000 * 60 * 60); } @bindThis diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts index ef87051a74..049e27dec8 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 { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } 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: KVCache; + private cache: MemoryKVCache; constructor( @Inject(DI.usersRepository) @@ -19,7 +19,7 @@ export class InstanceActorService { private createSystemUserService: CreateSystemUserService, ) { - this.cache = new KVCache(Infinity); + this.cache = new MemoryKVCache(Infinity); } @bindThis diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 7af7099432..552f241044 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 { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } 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 KVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); +const mutedWordsCache = new MemoryKVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 4537f1b81a..be5a4d4b02 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 { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } 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: KVCache; + private relaysCache: MemoryKVCache; constructor( @Inject(DI.usersRepository) @@ -30,7 +30,7 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new KVCache(1000 * 60 * 10); + this.relaysCache = new MemoryKVCache(1000 * 60 * 10); } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 7b63e43cb1..678bcfc337 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -2,7 +2,7 @@ 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 { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @@ -57,8 +57,8 @@ export const DEFAULT_POLICIES: RolePolicies = { @Injectable() export class RoleService implements OnApplicationShutdown { - private rolesCache: KVCache; - private roleAssignmentByUserIdCache: KVCache; + private rolesCache: MemoryKVCache; + private roleAssignmentByUserIdCache: MemoryKVCache; public static AlreadyAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {}; @@ -84,8 +84,8 @@ export class RoleService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.rolesCache = new KVCache(Infinity); - this.roleAssignmentByUserIdCache = new KVCache(Infinity); + this.rolesCache = new MemoryKVCache(Infinity); + this.roleAssignmentByUserIdCache = new MemoryKVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 33b51537a6..040b6de2ef 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -15,7 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { bindThis } from '@/decorators.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import { StreamMessages } from '@/server/api/stream/types.js'; @Injectable() @@ -23,7 +23,7 @@ export class UserBlockingService implements OnApplicationShutdown { private logger: Logger; // キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ - private blockingsByUserIdCache: KVCache; + private blockingsByUserIdCache: MemoryKVCache; constructor( @Inject(DI.redisSubscriber) @@ -58,7 +58,7 @@ export class UserBlockingService implements OnApplicationShutdown { ) { this.logger = this.loggerService.getLogger('user-block'); - this.blockingsByUserIdCache = new KVCache(Infinity); + this.blockingsByUserIdCache = new MemoryKVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 631eb44062..e452caf5d1 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import type { UsersRepository } from '@/models/index.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } 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'; @@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class UserCacheService implements OnApplicationShutdown { - public userByIdCache: KVCache; - public localUserByNativeTokenCache: KVCache; - public localUserByIdCache: KVCache; - public uriPersonCache: KVCache; + public userByIdCache: MemoryKVCache; + public localUserByNativeTokenCache: MemoryKVCache; + public localUserByIdCache: MemoryKVCache; + public uriPersonCache: MemoryKVCache; constructor( @Inject(DI.redisSubscriber) @@ -27,10 +27,10 @@ export class UserCacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new KVCache(Infinity); - this.localUserByNativeTokenCache = new KVCache(Infinity); - this.localUserByIdCache = new KVCache(Infinity); - this.uriPersonCache = new KVCache(Infinity); + this.userByIdCache = new MemoryKVCache(Infinity); + this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); + this.localUserByIdCache = new MemoryKVCache(Infinity); + this.uriPersonCache = new MemoryKVCache(Infinity); this.redisSubscriber.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserKeypairStoreService.ts b/packages/backend/src/core/UserKeypairStoreService.ts index 61c9293f86..872a0335ea 100644 --- a/packages/backend/src/core/UserKeypairStoreService.ts +++ b/packages/backend/src/core/UserKeypairStoreService.ts @@ -1,20 +1,20 @@ import { Inject, Injectable } from '@nestjs/common'; import type { User } from '@/models/entities/User.js'; import type { UserKeypairsRepository } from '@/models/index.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class UserKeypairStoreService { - private cache: KVCache; + private cache: MemoryKVCache; constructor( @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, ) { - this.cache = new KVCache(Infinity); + this.cache = new MemoryKVCache(Infinity); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index c3b3875613..dc0a865bbe 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -3,7 +3,7 @@ import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import type { Note } from '@/models/entities/Note.js'; @@ -31,8 +31,8 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService { - private publicKeyCache: KVCache; - private publicKeyByUserIdCache: KVCache; + private publicKeyCache: MemoryKVCache; + private publicKeyByUserIdCache: MemoryKVCache; constructor( @Inject(DI.config) @@ -50,8 +50,8 @@ export class ApDbResolverService { private userCacheService: UserCacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new KVCache(Infinity); - this.publicKeyByUserIdCache = new KVCache(Infinity); + this.publicKeyCache = new MemoryKVCache(Infinity); + this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); } @bindThis diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index ae7c47a990..71aa2ee6de 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -9,7 +9,7 @@ import type { Packed } from '@/misc/json-schema.js'; import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import type { Instance } from '@/models/entities/Instance.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; @@ -53,7 +53,7 @@ export class UserEntityService implements OnModuleInit { private customEmojiService: CustomEmojiService; private antennaService: AntennaService; private roleService: RoleService; - private userInstanceCache: KVCache; + private userInstanceCache: MemoryKVCache; constructor( private moduleRef: ModuleRef, @@ -119,7 +119,7 @@ export class UserEntityService implements OnModuleInit { //private antennaService: AntennaService, //private roleService: RoleService, ) { - this.userInstanceCache = new KVCache(1000 * 60 * 60 * 3); + this.userInstanceCache = new MemoryKVCache(1000 * 60 * 60 * 3); } onModuleInit() { diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index b249cf4480..a805d18421 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -2,11 +2,11 @@ import { bindThis } from '@/decorators.js'; // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? -export class KVCache { +export class MemoryKVCache { public cache: Map; private lifetime: number; - constructor(lifetime: KVCache['lifetime']) { + constructor(lifetime: MemoryKVCache['lifetime']) { this.cache = new Map(); this.lifetime = lifetime; } @@ -88,12 +88,12 @@ export class KVCache { } } -export class Cache { +export class MemoryCache { private cachedAt: number | null = null; private value: T | undefined; private lifetime: number; - constructor(lifetime: Cache['lifetime']) { + constructor(lifetime: MemoryCache['lifetime']) { this.lifetime = lifetime; } diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index f637bf8818..71865c778a 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 { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } 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: KVCache; + private suspendedHostsCache: MemoryKVCache; 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 KVCache(1000 * 60 * 60); + this.suspendedHostsCache = new MemoryKVCache(1000 * 60 * 60); } @bindThis diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 86019d4166..3387bd53aa 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 { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } 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,7 +118,7 @@ export class NodeinfoServerService { }; }; - const cache = new KVCache>>(1000 * 60 * 10); + const cache = new MemoryKVCache>>(1000 * 60 * 10); fastify.get(nodeinfo2_1path, async (request, reply) => { const base = await cache.fetch(null, () => nodeinfo2()); diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index a1895e3705..cd6bce9ef9 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js'; import type { LocalUser } from '@/models/entities/User.js'; import type { AccessToken } from '@/models/entities/AccessToken.js'; -import { KVCache } from '@/misc/cache.js'; +import { MemoryKVCache } from '@/misc/cache.js'; import type { App } from '@/models/entities/App.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import isNativeToken from '@/misc/is-native-token.js'; @@ -18,7 +18,7 @@ export class AuthenticationError extends Error { @Injectable() export class AuthenticateService { - private appCache: KVCache; + private appCache: MemoryKVCache; constructor( @Inject(DI.usersRepository) @@ -32,7 +32,7 @@ export class AuthenticateService { private userCacheService: UserCacheService, ) { - this.appCache = new KVCache(Infinity); + this.appCache = new MemoryKVCache(Infinity); } @bindThis -- cgit v1.2.3-freya From ecaf152b4a6eb702375debaef0dddc2cca798116 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 Apr 2023 17:32:09 +0900 Subject: enhance(backend): improve cache --- packages/backend/src/core/CacheService.ts | 95 +++++++++++++++++++++ packages/backend/src/core/CoreModule.ts | 12 +-- packages/backend/src/core/InstanceActorService.ts | 12 +-- packages/backend/src/core/NoteCreateService.ts | 6 +- packages/backend/src/core/NotificationService.ts | 18 ++-- packages/backend/src/core/RelayService.ts | 8 +- packages/backend/src/core/RoleService.ts | 30 +++---- packages/backend/src/core/UserCacheService.ts | 88 -------------------- .../src/core/activitypub/ApDbResolverService.ts | 10 +-- .../src/core/activitypub/models/ApPersonService.ts | 14 ++-- .../src/core/entities/NotificationEntityService.ts | 1 + .../backend/src/core/entities/UserEntityService.ts | 1 - packages/backend/src/misc/cache.ts | 97 ++++++++++++++++++++-- .../queue/processors/DeliverProcessorService.ts | 10 +-- .../backend/src/server/NodeinfoServerService.ts | 8 +- .../backend/src/server/api/AuthenticateService.ts | 8 +- .../src/server/api/endpoints/i/regenerate-token.ts | 2 +- .../backend/src/server/api/endpoints/i/update.ts | 8 +- .../src/server/api/endpoints/mute/create.ts | 3 + .../src/server/api/endpoints/renote-mute/create.ts | 2 - packages/backend/src/server/api/stream/types.ts | 2 +- packages/backend/test/unit/RoleService.ts | 4 +- 22 files changed, 267 insertions(+), 172 deletions(-) create mode 100644 packages/backend/src/core/CacheService.ts delete mode 100644 packages/backend/src/core/UserCacheService.ts (limited to 'packages/backend/src/server') diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts new file mode 100644 index 0000000000..887baeb2c2 --- /dev/null +++ b/packages/backend/src/core/CacheService.ts @@ -0,0 +1,95 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; +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'; +import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +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, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private userEntityService: UserEntityService, + ) { + //this.onMessage = this.onMessage.bind(this); + + this.userByIdCache = new MemoryKVCache(Infinity); + 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); + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as StreamMessages['internal']['payload']; + switch (type) { + case 'userChangeSuspendedState': + case 'remoteUserUpdated': { + const user = await this.usersRepository.findOneByOrFail({ id: body.id }); + this.userByIdCache.set(user.id, user); + for (const [k, v] of this.uriPersonCache.cache.entries()) { + if (v.value?.id === user.id) { + this.uriPersonCache.set(k, user); + } + } + if (this.userEntityService.isLocalUser(user)) { + this.localUserByNativeTokenCache.set(user.token!, user); + this.localUserByIdCache.set(user.id, user); + } + break; + } + case 'userTokenRegenerated': { + const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser; + this.localUserByNativeTokenCache.delete(body.oldToken); + this.localUserByNativeTokenCache.set(body.newToken, user); + break; + } + case 'follow': { + const follower = this.userByIdCache.get(body.followerId); + if (follower) follower.followingCount++; + const followee = this.userByIdCache.get(body.followeeId); + if (followee) followee.followersCount++; + break; + } + default: + break; + } + } + } + + @bindThis + public findUserById(userId: User['id']) { + return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined) { + this.redisSubscriber.off('message', this.onMessage); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index d67e80fc1d..5c867e6cfc 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 049e27dec8..898fb4ce85 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 552f241044..83290b310e 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 2a4dbba6a4..9c179f9318 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 be5a4d4b02..4df7fb3bff 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 678bcfc337..52e6292a1e 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/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts deleted file mode 100644 index e452caf5d1..0000000000 --- a/packages/backend/src/core/UserCacheService.ts +++ /dev/null @@ -1,88 +0,0 @@ -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 { LocalUser, User } from '@/models/entities/User.js'; -import { DI } from '@/di-symbols.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { bindThis } from '@/decorators.js'; -import { StreamMessages } from '@/server/api/stream/types.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; - -@Injectable() -export class UserCacheService implements OnApplicationShutdown { - public userByIdCache: MemoryKVCache; - public localUserByNativeTokenCache: MemoryKVCache; - public localUserByIdCache: MemoryKVCache; - public uriPersonCache: MemoryKVCache; - - constructor( - @Inject(DI.redisSubscriber) - private redisSubscriber: Redis.Redis, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private userEntityService: UserEntityService, - ) { - //this.onMessage = this.onMessage.bind(this); - - this.userByIdCache = new MemoryKVCache(Infinity); - this.localUserByNativeTokenCache = new MemoryKVCache(Infinity); - this.localUserByIdCache = new MemoryKVCache(Infinity); - this.uriPersonCache = new MemoryKVCache(Infinity); - - this.redisSubscriber.on('message', this.onMessage); - } - - @bindThis - private async onMessage(_: string, data: string): Promise { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as StreamMessages['internal']['payload']; - switch (type) { - case 'userChangeSuspendedState': - case 'remoteUserUpdated': { - const user = await this.usersRepository.findOneByOrFail({ id: body.id }); - this.userByIdCache.set(user.id, user); - for (const [k, v] of this.uriPersonCache.cache.entries()) { - if (v.value?.id === user.id) { - this.uriPersonCache.set(k, user); - } - } - if (this.userEntityService.isLocalUser(user)) { - this.localUserByNativeTokenCache.set(user.token, user); - this.localUserByIdCache.set(user.id, user); - } - break; - } - case 'userTokenRegenerated': { - const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser; - this.localUserByNativeTokenCache.delete(body.oldToken); - this.localUserByNativeTokenCache.set(body.newToken, user); - break; - } - case 'follow': { - const follower = this.userByIdCache.get(body.followerId); - if (follower) follower.followingCount++; - const followee = this.userByIdCache.get(body.followeeId); - if (followee) followee.followersCount++; - break; - } - default: - break; - } - } - } - - @bindThis - public findById(userId: User['id']) { - return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId })); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined) { - this.redisSubscriber.off('message', this.onMessage); - } -} diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index dc0a865bbe..4b032be89a 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 41f7eafa41..67e907c271 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 7cffb8d568..6b9a9d3320 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 71aa2ee6de..e8474c7e0e 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 a805d18421..870dfd237c 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 71865c778a..a9af22ad09 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 3387bd53aa..66c1faaac2 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 cd6bce9ef9..6548c475b2 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 f942f43cc8..786e64374c 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 b1eaab3908..46b16e9dce 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 9099eea52e..fd062e1cab 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 051a005b67..b285269617 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 b8f50e0546..1e6e51e76d 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 6fe04274e6..907f1f2edc 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, ], -- cgit v1.2.3-freya