enhance(backend): RedisへのTLのキャッシュをオフにできるように

This commit is contained in:
syuilo 2023-10-23 15:17:25 +09:00
parent 5a39c1a8eb
commit e6c54de814
14 changed files with 515 additions and 371 deletions

View file

@ -25,6 +25,7 @@
- Fix: 投稿フォームでのユーザー変更がプレビューに反映されない問題を修正
### Server
- Enhance: RedisへのTLのキャッシュをオフにできるように
- Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正
- Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正

1
locales/index.d.ts vendored
View file

@ -1190,6 +1190,7 @@ export interface Locale {
"manifestJsonOverride": string;
"shortName": string;
"shortNameDescription": string;
"fanoutTimelineDescription": string;
};
"_accountMigration": {
"moveFrom": string;

View file

@ -1188,6 +1188,7 @@ _serverSettings:
manifestJsonOverride: "manifest.jsonのオーバーライド"
shortName: "略称"
shortNameDescription: "サーバーの正式名称が長い場合に、代わりに表示することのできる略称や通称。"
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class EnableFtt1698041201306 {
name = 'EnableFtt1698041201306'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimeline" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimeline"`);
}
}

View file

@ -825,6 +825,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
const meta = await this.metaService.fetch();
if (!meta.enableFanoutTimeline) return;
const r = this.redisForTimelines.pipeline();

View file

@ -40,7 +40,7 @@ export class QueryService {
) {
}
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });

View file

@ -489,6 +489,11 @@ export class MiMeta {
})
public preservedUsernames: string[];
@Column('boolean', {
default: true,
})
public enableFanoutTimeline: boolean;
@Column('integer', {
default: 300,
})

View file

@ -106,11 +106,11 @@ export const meta = {
optional: false, nullable: false,
},
silencedHosts: {
type: "array",
type: 'array',
optional: true,
nullable: false,
items: {
type: "string",
type: 'string',
optional: false,
nullable: false,
},
@ -291,6 +291,10 @@ export const meta = {
type: 'object',
optional: false, nullable: false,
},
enableFanoutTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
perLocalUserUserTimelineCacheMax: {
type: 'number',
optional: false, nullable: false,
@ -419,6 +423,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableIdenticonGeneration: instance.enableIdenticonGeneration,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
manifestJsonOverride: instance.manifestJsonOverride,
enableFanoutTimeline: instance.enableFanoutTimeline,
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,

View file

@ -120,6 +120,7 @@ export const paramDef = {
serverRules: { type: 'array', items: { type: 'string' } },
preservedUsernames: { type: 'array', items: { type: 'string' } },
manifestJsonOverride: { type: 'string' },
enableFanoutTimeline: { type: 'boolean' },
perLocalUserUserTimelineCacheMax: { type: 'integer' },
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
@ -480,6 +481,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.manifestJsonOverride = ps.manifestJsonOverride;
}
if (ps.enableFanoutTimeline !== undefined) {
set.enableFanoutTimeline = ps.enableFanoutTimeline;
}
if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
}

View file

@ -17,6 +17,8 @@ import { CacheService } from '@/core/CacheService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -75,6 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private funoutTimelineService: FunoutTimelineService,
private queryService: QueryService,
private userFollowingService: UserFollowingService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -85,6 +88,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.stlDisabled);
}
const serverSettings = await this.metaService.fetch();
if (serverSettings.enableFanoutTimeline) {
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
@ -163,9 +169,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.noteEntityService.packMany(timeline, me);
} else { // fallback to db
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
}, me);
}
} else {
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
}, me);
}
});
}
private async getFromDb(ps: {
untilId: string | null,
sinceId: string | null,
limit: number,
includeMyRenotes: boolean,
includeRenotedMyNotes: boolean,
includeLocalRenotes: boolean,
withFiles: boolean,
withReplies: boolean,
}, me: MiLocalUser) {
const followees = await this.userFollowingService.getFollowees(me.id);
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => {
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
@ -242,6 +284,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.noteEntityService.packMany(timeline, me);
}
});
}
}

View file

@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -69,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService,
private queryService: QueryService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -79,6 +82,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.ltlDisabled);
}
const serverSettings = await this.metaService.fetch();
if (serverSettings.enableFanoutTimeline) {
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,
@ -145,8 +151,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.noteEntityService.packMany(timeline, me);
} else { // fallback to db
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
}, me);
}
} else {
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
withFiles: ps.withFiles,
withReplies: ps.withReplies,
}, me);
}
});
}
private async getFromDb(ps: {
sinceId: string | null,
untilId: string | null,
limit: number,
withFiles: boolean,
withReplies: boolean,
}, me: MiLocalUser | null) {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
ps.sinceId, ps.untilId)
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
@ -185,6 +218,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.noteEntityService.packMany(timeline, me);
}
});
}
}

View file

@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['notes'],
@ -63,11 +65,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private funoutTimelineService: FunoutTimelineService,
private userFollowingService: UserFollowingService,
private queryService: QueryService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const serverSettings = await this.metaService.fetch();
if (serverSettings.enableFanoutTimeline) {
const [
followings,
userIdsWhoMeMuting,
@ -124,10 +130,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.noteEntityService.packMany(timeline, me);
} else { // fallback to db
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
}, me);
}
} else {
return await this.getFromDb({
untilId,
sinceId,
limit: ps.limit,
includeMyRenotes: ps.includeMyRenotes,
includeRenotedMyNotes: ps.includeRenotedMyNotes,
includeLocalRenotes: ps.includeLocalRenotes,
withFiles: ps.withFiles,
}, me);
}
});
}
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; }, me: MiLocalUser) {
const followees = await this.userFollowingService.getFollowees(me.id);
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.channelId IS NULL')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
@ -201,6 +232,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await this.noteEntityService.packMany(timeline, me);
}
});
}
}

View file

@ -93,6 +93,7 @@ describe('ActivityPub', () => {
const metaInitial = {
cacheRemoteFiles: true,
cacheRemoteSensitiveFiles: true,
enableFanoutTimeline: true,
perUserHomeTimelineCacheMax: 100,
perLocalUserUserTimelineCacheMax: 100,
perRemoteUserUserTimelineCacheMax: 100,

View file

@ -87,9 +87,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSection>
<FormSection>
<template #label>Timeline caching</template>
<template #label>Misskey® Fan-out Timeline Technology (FTT)</template>
<div class="_gaps_m">
<MkSwitch v-model="enableFanoutTimeline">
<template #label>{{ i18n.ts.enable }}</template>
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template>
</MkSwitch>
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax</template>
</MkInput>
@ -165,6 +170,7 @@ let cacheRemoteSensitiveFiles: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false);
let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null);
let enableFanoutTimeline: boolean = $ref(false);
let perLocalUserUserTimelineCacheMax: number = $ref(0);
let perRemoteUserUserTimelineCacheMax: number = $ref(0);
let perUserHomeTimelineCacheMax: number = $ref(0);
@ -185,6 +191,7 @@ async function init(): Promise<void> {
enableServiceWorker = meta.enableServiceWorker;
swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey;
enableFanoutTimeline = meta.enableFanoutTimeline;
perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax;
perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
@ -206,6 +213,7 @@ async function save(): void {
enableServiceWorker,
swPublicKey,
swPrivateKey,
enableFanoutTimeline,
perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax,