perf(backend): cache timeline of a channel to redis
This commit is contained in:
parent
a0c3dede8e
commit
4f9f625e65
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ulid } from 'ulid';
|
import { ulid } from 'ulid';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { genAid } from '@/misc/id/aid.js';
|
import { genAid, parseAid } from '@/misc/id/aid.js';
|
||||||
import { genMeid } from '@/misc/id/meid.js';
|
import { genMeid } from '@/misc/id/meid.js';
|
||||||
import { genMeidg } from '@/misc/id/meidg.js';
|
import { genMeidg } from '@/misc/id/meidg.js';
|
||||||
import { genObjectId } from '@/misc/id/object-id.js';
|
import { genObjectId } from '@/misc/id/object-id.js';
|
||||||
|
@ -32,4 +32,17 @@ export class IdService {
|
||||||
default: throw new Error('unrecognized id generation method');
|
default: throw new Error('unrecognized id generation method');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public parse(id: string): { date: Date; } {
|
||||||
|
switch (this.method) {
|
||||||
|
case 'aid': return parseAid(id);
|
||||||
|
// TODO
|
||||||
|
//case 'meid':
|
||||||
|
//case 'meidg':
|
||||||
|
//case 'ulid':
|
||||||
|
//case 'objectid':
|
||||||
|
default: throw new Error('unrecognized id generation method');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { setImmediate } from 'node:timers/promises';
|
import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource } from 'typeorm';
|
import { In, DataSource } from 'typeorm';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
|
@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -321,6 +325,10 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
||||||
|
|
||||||
|
if (data.channel) {
|
||||||
|
this.redisClient.xadd(`channelTimeline:${data.channel.id}`, 'MAXLEN', '~', '1000', `${this.idService.parse(note.id).date.getTime()}-*`, 'note', note.id);
|
||||||
|
}
|
||||||
|
|
||||||
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||||
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
||||||
() => { /* aborted, ignore this */ },
|
() => { /* aborted, ignore this */ },
|
||||||
|
|
|
@ -23,3 +23,8 @@ export function genAid(date: Date): string {
|
||||||
counter++;
|
counter++;
|
||||||
return getTime(t) + getNoise();
|
return getTime(t) + getNoise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseAid(id: string): { date: Date; } {
|
||||||
|
const time = parseInt(id.slice(0, 8), 36) + TIME2000;
|
||||||
|
return { date: new Date(time) };
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
|
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -48,12 +50,16 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.channelsRepository)
|
@Inject(DI.channelsRepository)
|
||||||
private channelsRepository: ChannelsRepository,
|
private channelsRepository: ChannelsRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
|
@ -67,9 +73,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchChannel);
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noteIdsRes = await this.redisClient.xrevrange(
|
||||||
|
`channelTimeline:${channel.id}`,
|
||||||
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
||||||
|
|
||||||
|
if (noteIdsRes.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
|
||||||
|
|
||||||
|
if (noteIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
//#region Construct query
|
//#region Construct query
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
.andWhere('note.channelId = :channelId', { channelId: channel.id })
|
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
|
@ -90,7 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const timeline = await query.take(ps.limit).getMany();
|
const timeline = await query.getMany();
|
||||||
|
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||||
|
|
||||||
if (me) this.activeUsersChart.read(me);
|
if (me) this.activeUsersChart.read(me);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue