fix public key caching

This commit is contained in:
Hazelnoot 2025-10-01 12:21:57 -04:00
parent 57511f26d9
commit 6ff3d506b5
2 changed files with 64 additions and 78 deletions

View file

@ -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,

View file

@ -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<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
private readonly publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
private publicKeyByKeyIdCache: ManagedQuantumKVCache<MiUserPublickey>;
private publicKeyByUserIdCache: ManagedQuantumKVCache<MiUserPublickey>;
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<MiUserPublickey>('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<MiUserPublickey>('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<void> {
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<boolean> {
private async isPublicCollection(collection: string | IObject | undefined, resolver: Resolver, sentFrom: string): Promise<boolean> {
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<MiUserPublickey | null> {
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<MiUserPublickey | null> {
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;
}
}