Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(backend): EntityServiceなどでDB照会の結果を短時間キャッシュするように #477

Open
wants to merge 1 commit into
base: io
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"lru-cache": "10.2.0",
"meilisearch": "0.37.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
Expand Down Expand Up @@ -197,7 +198,7 @@
"@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3",
"@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.5",
"@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
Expand Down
45 changes: 45 additions & 0 deletions packages/backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export type RedisOptionsSource = Partial<RedisOptions> & {
prefix?: string;
};

export type MemoryCacheOptions = {
max?: number;
ttl?: number;
};

/**
* 設定ファイルの型
*/
Expand Down Expand Up @@ -66,6 +71,20 @@ type Source = {
scope?: 'local' | 'global' | string[];
};

memoryCache?: {
database?: MemoryCacheOptions;
user?: MemoryCacheOptions;
userProfile?: MemoryCacheOptions;
notification?: MemoryCacheOptions;
relation?: MemoryCacheOptions;
role?: MemoryCacheOptions;
channel?: MemoryCacheOptions;
driveFile?: MemoryCacheOptions;
note?: MemoryCacheOptions;
poll?: MemoryCacheOptions;
page?: MemoryCacheOptions;
};

proxy?: string;
proxySmtp?: string;
proxyBypassHosts?: string[];
Expand Down Expand Up @@ -137,6 +156,19 @@ export type Config = {
index: string;
scope?: 'local' | 'global' | string[];
} | undefined;
memoryCache: {
database: MemoryCacheOptions;
user: MemoryCacheOptions;
userProfile: MemoryCacheOptions;
notification: MemoryCacheOptions;
relation: MemoryCacheOptions;
role: MemoryCacheOptions;
channel: MemoryCacheOptions;
driveFile: MemoryCacheOptions;
note: MemoryCacheOptions;
poll: MemoryCacheOptions;
page: MemoryCacheOptions;
};
proxy: string | undefined;
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
Expand Down Expand Up @@ -261,6 +293,19 @@ export function loadConfig(): Config {
redisForObjectStorageQueue: config.redisForObjectStorageQueue ? convertRedisOptions(config.redisForObjectStorageQueue, host) : redisForJobQueue,
redisForWebhookDeliverQueue: config.redisForWebhookDeliverQueue ? convertRedisOptions(config.redisForWebhookDeliverQueue, host) : redisForJobQueue,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
memoryCache: {
database: config.memoryCache?.database ?? { max: undefined, ttl: undefined },
user: config.memoryCache?.user ?? { max: undefined, ttl: undefined },
userProfile: config.memoryCache?.userProfile ?? { max: undefined, ttl: undefined },
notification: config.memoryCache?.notification ?? { max: undefined, ttl: undefined },
relation: config.memoryCache?.relation ?? { max: undefined, ttl: undefined },
role: config.memoryCache?.role ?? { max: undefined, ttl: undefined },
channel: config.memoryCache?.channel ?? { max: undefined, ttl: undefined },
driveFile: config.memoryCache?.driveFile ?? { max: undefined, ttl: undefined },
note: config.memoryCache?.note ?? { max: undefined, ttl: undefined },
poll: config.memoryCache?.poll ?? { max: undefined, ttl: undefined },
page: config.memoryCache?.page ?? { max: undefined, ttl: undefined },
},
id: config.id,
proxy: config.proxy,
proxySmtp: config.proxySmtp,
Expand Down
25 changes: 25 additions & 0 deletions packages/backend/src/core/CacheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class CacheService implements OnApplicationShutdown {
case 'userChangeDeletedState':
case 'remoteUserUpdated':
case 'localUserUpdated': {
this.userEntityService.purgeCache(body.id);
const user = await this.usersRepository.findOneBy({ id: body.id });
if (user == null) {
this.userByIdCache.delete(body.id);
Expand All @@ -155,6 +156,7 @@ export class CacheService implements OnApplicationShutdown {
break;
}
case 'userTokenRegenerated': {
this.userEntityService.purgeCache(body.id);
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as MiLocalUser;
this.localUserByNativeTokenCache.delete(body.oldToken);
this.localUserByNativeTokenCache.set(body.newToken, user);
Expand All @@ -171,6 +173,29 @@ export class CacheService implements OnApplicationShutdown {
default:
break;
}
} else if (obj.channel.startsWith('mainStream:')) {
const { type } = obj.message as GlobalEvents['main']['payload'];
switch (type) {
case 'meUpdated':
case 'readAllNotifications':
case 'unreadNotification':
case 'unreadMention':
case 'readAllUnreadMentions':
case 'unreadSpecifiedNote':
case 'readAllUnreadSpecifiedNotes':
case 'readAllAntennas':
case 'unreadAntenna':
case 'readAllAnnouncements':
case 'readAntenna':
case 'receiveFollowRequest':
case 'announcementCreated': {
const userId = obj.channel.slice(11);
this.userEntityService.purgeCache(userId);
break;
}
default:
break;
}
}
}

Expand Down
38 changes: 34 additions & 4 deletions packages/backend/src/core/entities/ChannelEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,30 @@
*/

import { Inject, Injectable } from '@nestjs/common';
import { LRUCache } from 'lru-cache';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NotesRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
import type { MiChannel } from '@/models/Channel.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { cacheFetch, cacheFetchOrFail } from '@/misc/cache-fetch.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { NoteEntityService } from './NoteEntityService.js';

@Injectable()
export class ChannelEntityService {
private readonly channelCache: LRUCache<string, MiChannel>;
private readonly bannerCache: LRUCache<string, MiDriveFile>;

constructor(
@Inject(DI.config)
private config: Config,

@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,

Expand All @@ -37,6 +47,27 @@ export class ChannelEntityService {
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
) {
this.channelCache = new LRUCache({
max: this.config.memoryCache.channel.max ?? 1,
ttl: this.config.memoryCache.channel.ttl ?? 100,

ignoreFetchAbort: true,
ttlResolution: 1000 * 30,
fetchMethod: async (id: string) => {
return await this.channelsRepository.findOneByOrFail({ id });
},
});

this.bannerCache = new LRUCache({
max: this.config.memoryCache.driveFile.max ?? 1,
ttl: this.config.memoryCache.driveFile.ttl ?? 100,

ignoreFetchAbort: true,
ttlResolution: 1000 * 30,
fetchMethod: async (id: string) => {
return await this.driveFilesRepository.findOneByOrFail({ id });
},
});
}

@bindThis
Expand All @@ -45,11 +76,10 @@ export class ChannelEntityService {
me: { id: MiUser['id'] } | null | undefined,
detailed?: boolean,
): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
const meId = me ? me.id : null;

const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
const channel = typeof src === 'object' ? src : await cacheFetchOrFail(this.channelCache, src);
const banner = channel.bannerId ? await cacheFetch(this.bannerCache, channel.bannerId) : null;

const meId = me ? me.id : null;
const isFollowing = meId ? await this.channelFollowingsRepository.exists({
where: {
followerId: meId,
Expand Down
24 changes: 19 additions & 5 deletions packages/backend/src/core/entities/DriveFileEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { LRUCache } from 'lru-cache';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import { appendQuery, query } from '@/misc/prelude/url.js';
Expand All @@ -18,6 +19,7 @@ import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdService } from '@/core/IdService.js';
import { cacheFetch, cacheFetchOrFail } from '@/misc/cache-fetch.js';
import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js';
import { UserEntityService } from './UserEntityService.js';
Expand All @@ -31,6 +33,8 @@ type PackOptions = {

@Injectable()
export class DriveFileEntityService {
private readonly driveFileCache: LRUCache<string, MiDriveFile>;

constructor(
@Inject(DI.config)
private config: Config,
Expand All @@ -47,6 +51,16 @@ export class DriveFileEntityService {
private videoProcessingService: VideoProcessingService,
private idService: IdService,
) {
this.driveFileCache = new LRUCache({
max: this.config.memoryCache.driveFile.max ?? 1,
ttl: this.config.memoryCache.driveFile.ttl ?? 100,

ignoreFetchAbort: true,
ttlResolution: 1000 * 30,
fetchMethod: async (id: string) => {
return await this.driveFilesRepository.findOneByOrFail({ id });
},
});
}

@bindThis
Expand Down Expand Up @@ -195,7 +209,7 @@ export class DriveFileEntityService {
self: false,
}, options);

const file = typeof src === 'object' ? src : await this.driveFilesRepository.findOneByOrFail({ id: src });
const file = typeof src === 'object' ? src : await cacheFetchOrFail(this.driveFileCache, src);

return await awaitAll<Packed<'DriveFile'>>({
id: file.id,
Expand Down Expand Up @@ -230,7 +244,7 @@ export class DriveFileEntityService {
self: false,
}, options);

const file = typeof src === 'object' ? src : await this.driveFilesRepository.findOneBy({ id: src });
const file = typeof src === 'object' ? src : await cacheFetch(this.driveFileCache, src);
if (file == null) return null;

return await awaitAll<Packed<'DriveFile'>>({
Expand Down Expand Up @@ -273,7 +287,7 @@ export class DriveFileEntityService {
options?: PackOptions,
): Promise<Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>> {
if (fileIds.length === 0) return new Map();
const files = await this.driveFilesRepository.findBy({ id: In(fileIds) });
const files = await this.driveFilesRepository.find({ where: { id: In(fileIds) }, cache: this.config.memoryCache.driveFile.ttl });
const packedFiles = await this.packMany(files, me, options);
const map = new Map<Packed<'DriveFile'>['id'], Packed<'DriveFile'> | null>(packedFiles.map(f => [f.id, f]));
for (const id of fileIds) {
Expand Down
Loading
Loading