diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index f1dccb6dae..2516ba5141 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -3,12 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { In } from 'typeorm'; import { UnrecoverableError } from 'bullmq'; import promiseLimit from 'promise-limit'; +import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; +import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -40,28 +41,28 @@ import { ApDbResolverService } from '../ApDbResolverService.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApAudienceService } from '../ApAudienceService.js'; import { ApUtilityService } from '../ApUtilityService.js'; -import { ApPersonService } from './ApPersonService.js'; import { extractApHashtags } from './tag.js'; import { ApMentionService } from './ApMentionService.js'; import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; +import type { ApPersonService } from './ApPersonService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost, IApEmoji } from '../type.js'; @Injectable() -export class ApNoteService { +export class ApNoteService implements OnModuleInit { + private apPersonService: ApPersonService; private logger: Logger; constructor( + private readonly moduleRef: ModuleRef, + @Inject(DI.config) private config: Config, @Inject(DI.meta) private meta: MiMeta, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -75,10 +76,6 @@ export class ApNoteService { private apMfmService: ApMfmService, private apResolverService: ApResolverService, - // 循環参照のため / for circular dependency - @Inject(forwardRef(() => ApPersonService)) - private apPersonService: ApPersonService, - private utilityService: UtilityService, private apAudienceService: ApAudienceService, private apMentionService: ApMentionService, @@ -96,6 +93,11 @@ export class ApNoteService { this.logger = this.apLoggerService.logger; } + @bindThis + public onModuleInit() { + this.apPersonService = this.moduleRef.get('ApPersonService'); + } + @bindThis public validateNote( object: IObject, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 91ae0117dc..b5564575b1 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -3,23 +3,25 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import { DataSource } from 'typeorm'; +import { DataSource, In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { isRemoteUser, isLocalUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js'; import { truncate } from '@/misc/truncate.js'; import type { CacheService } from '@/core/CacheService.js'; +import { CacheManagementService, type ManagedQuantumKVCache } from '@/core/CacheManagementService.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'; import type { MiNote } from '@/models/Note.js'; -import type { IdService } from '@/core/IdService.js'; +import { IdService } from '@/core/IdService.js'; import type { MfmService } from '@/core/MfmService.js'; import { toArray } from '@/misc/prelude/array.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -31,27 +33,25 @@ import type UsersChart from '@/core/chart/charts/users.js'; import type InstanceChart from '@/core/chart/charts/instance.js'; import type { HashtagService } from '@/core/HashtagService.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; -import type { UtilityService } from '@/core/UtilityService.js'; -import type { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import type { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { AppLockService } from '@/core/AppLockService.js'; -import { MemoryKVCache } from '@/misc/cache.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { verifyFieldLinks } from '@/misc/verify-field-link.js'; import { isRetryableError } from '@/misc/is-retryable-error.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; +import { ApLoggerService } from '../ApLoggerService.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; import type { ApNoteService } from './ApNoteService.js'; import type { ApMfmService } from '../ApMfmService.js'; import type { ApResolverService, Resolver } from '../ApResolverService.js'; -import type { ApLoggerService } from '../ApLoggerService.js'; import type { ApImageService } from './ApImageService.js'; import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; @@ -61,15 +61,12 @@ const nameLength = 128; type Field = Record<'name' | 'value', string>; @Injectable() -export class ApPersonService implements OnModuleInit, OnApplicationShutdown { +export class ApPersonService implements OnModuleInit { // Moved from ApDbResolverService - private readonly publicKeyByKeyIdCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h - private readonly publicKeyByUserIdCache = new MemoryKVCache(1000 * 60 * 60 * 12); // 12h + private publicKeyByKeyIdCache: ManagedQuantumKVCache; + private publicKeyByUserIdCache: ManagedQuantumKVCache; - private utilityService: UtilityService; - private userEntityService: UserEntityService; private driveFileEntityService: DriveFileEntityService; - private idService: IdService; private globalEventService: GlobalEventService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; @@ -82,7 +79,6 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { private hashtagService: HashtagService; private usersChart: UsersChart; private instanceChart: InstanceChart; - private apLoggerService: ApLoggerService; private accountMoveService: AccountMoveService; private logger: Logger; @@ -114,18 +110,31 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { private followingsRepository: FollowingsRepository, private roleService: RoleService, - private readonly apUtilityService: ApUtilityService, private readonly httpRequestService: HttpRequestService, private readonly appLockService: AppLockService, + private readonly cacheManagementService: CacheManagementService, + private readonly utilityService: UtilityService, + private readonly apUtilityService: ApUtilityService, + private readonly idService: IdService, + + apLoggerService: ApLoggerService, ) { + this.logger = apLoggerService.logger; + this.publicKeyByKeyIdCache = this.cacheManagementService.createQuantumKVCache('publicKeyByKeyId', { + lifetime: 1000 * 60 * 60 * 12, // 12h + fetcher: async (keyId) => await this.userPublickeysRepository.findOneBy({ keyId }), + bulkFetcher: async (keyIds) => await this.userPublickeysRepository.findBy({ keyId: In(keyIds) }).then(ks => ks.map(k => [k.keyId, k])), + }); + this.publicKeyByUserIdCache = this.cacheManagementService.createQuantumKVCache('publicKeyByUserId', { + lifetime: 1000 * 60 * 60 * 12, // 12h + fetcher: async (userId) => await this.userPublickeysRepository.findOneBy({ userId }), + bulkFetcher: async (userIds) => await this.userPublickeysRepository.findBy({ userId: In(userIds) }).then(ks => ks.map(k => [k.userId, k])), + }); } @bindThis onModuleInit(): void { - this.utilityService = this.moduleRef.get('UtilityService'); - this.userEntityService = this.moduleRef.get('UserEntityService'); this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); - this.idService = this.moduleRef.get('IdService'); this.globalEventService = this.moduleRef.get('GlobalEventService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); @@ -138,14 +147,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { this.hashtagService = this.moduleRef.get('HashtagService'); this.usersChart = this.moduleRef.get('UsersChart'); this.instanceChart = this.moduleRef.get('InstanceChart'); - this.apLoggerService = this.moduleRef.get('ApLoggerService'); this.accountMoveService = this.moduleRef.get('AccountMoveService'); - this.logger = this.apLoggerService.logger; - } - - @bindThis - onApplicationShutdown(): void { - this.dispose(); } /** @@ -507,9 +509,12 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { this.cacheService.uriPersonCache.set(user.uri, user); // Register public key to the cache. - // Value may be null, which indicates that the user has no defined key. (optimization) - this.publicKeyByUserIdCache.set(user.id, publicKey); - if (publicKey) this.publicKeyByKeyIdCache.set(publicKey.keyId, publicKey); + if (publicKey) { + await Promise.all([ + this.publicKeyByKeyIdCache.set(publicKey.keyId, publicKey), + this.publicKeyByUserIdCache.set(publicKey.userId, publicKey), + ]); + } // Register host if (this.meta.enableStatsForFederatedInstances) { @@ -700,18 +705,21 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { // Create or update key await this.userPublickeysRepository.save(publicKey); - this.publicKeyByKeyIdCache.set(person.publicKey.id, publicKey); - this.publicKeyByUserIdCache.set(exist.id, publicKey); + // Save it to the cache + await Promise.all([ + this.publicKeyByKeyIdCache.set(publicKey.keyId, publicKey), + this.publicKeyByUserIdCache.set(publicKey.userId, publicKey), + ]); } else { const existingPublicKey = await this.userPublickeysRepository.findOneBy({ userId: exist.id }); if (existingPublicKey) { // Delete key - await this.userPublickeysRepository.delete({ userId: exist.id }); - this.publicKeyByKeyIdCache.delete(existingPublicKey.keyId); + await Promise.all([ + this.userPublickeysRepository.delete({ userId: existingPublicKey.userId }), + this.publicKeyByUserIdCache.delete(existingPublicKey.userId), + this.publicKeyByKeyIdCache.delete(existingPublicKey.keyId), + ]); } - - // Null indicates that the user has no key. (optimization) - this.publicKeyByUserIdCache.set(exist.id, null); } let _description: string | null = null; @@ -843,7 +851,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { @bindThis public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise { const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false }); - if (!this.userEntityService.isRemoteUser(user)) return; + if (!isRemoteUser(user)) return; if (!user.featured) return; this.logger.info(`Updating the featured: ${user.uri}`); @@ -908,7 +916,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { // まずサーバー内で検索して様子見 let dst = await this.fetchPerson(src.movedToUri); - if (dst && this.userEntityService.isLocalUser(dst)) { + if (dst && isLocalUser(dst)) { // targetがローカルユーザーだった場合データベースから引っ張ってくる dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as MiLocalUser; } else if (dst) { @@ -944,10 +952,10 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { } @bindThis - private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver, sentFrom: string): Promise { + private async isPublicCollection(collection: string | IObject | undefined, resolver: Resolver, sentFrom: string): Promise { if (collection) { - const resolved = await resolver.resolveCollection(collection, true, sentFrom).catch(() => null); - if (resolved) { + const resolved = await resolver.resolveCollection(collection, true, sentFrom); + { if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) { return true; } @@ -959,35 +967,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { @bindThis public async findPublicKeyByUserId(userId: string): Promise { - const publicKey = this.publicKeyByUserIdCache.get(userId) ?? await this.userPublickeysRepository.findOneBy({ userId }); - - // This can technically keep a key cached "forever" if it's used enough, but that's ok. - // We can never have stale data because the publicKey caches are coherent. (cache updates whenever data changes) - if (publicKey) { - this.publicKeyByUserIdCache.set(publicKey.userId, publicKey); - this.publicKeyByKeyIdCache.set(publicKey.keyId, publicKey); - } - - return publicKey; + return await this.publicKeyByUserIdCache.fetchMaybe(userId) ?? null; } @bindThis public async findPublicKeyByKeyId(keyId: string): Promise { - const publicKey = this.publicKeyByKeyIdCache.get(keyId) ?? await this.userPublickeysRepository.findOneBy({ keyId }); - - // This can technically keep a key cached "forever" if it's used enough, but that's ok. - // We can never have stale data because the publicKey caches are coherent. (cache updates whenever data changes) - if (publicKey) { - this.publicKeyByUserIdCache.set(publicKey.userId, publicKey); - this.publicKeyByKeyIdCache.set(publicKey.keyId, publicKey); - } - - return publicKey; - } - - @bindThis - public dispose(): void { - this.publicKeyByUserIdCache.dispose(); - this.publicKeyByKeyIdCache.dispose(); + return await this.publicKeyByKeyIdCache.fetchMaybe(keyId) ?? null; } }