fix emoji caching

This commit is contained in:
Hazelnoot 2025-10-01 12:05:43 -04:00
parent df5aff9d91
commit bb0925224d
14 changed files with 552 additions and 288 deletions

View file

@ -36,6 +36,7 @@ import { IActivity, IAnnounce, ICreate } from '@/core/activitypub/type.js';
import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import { CacheService } from '@/core/CacheService.js';
import { CustomEmojiService, encodeEmojiKey } from '@/core/CustomEmojiService.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
@ -89,6 +90,7 @@ export class ActivityPubServerService {
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private loggerService: LoggerService,
private readonly cacheService: CacheService,
private readonly customEmojiService: CustomEmojiService,
) {
//this.createServer = this.createServer.bind(this);
this.logger = this.loggerService.getLogger('apserv', 'pink');
@ -1037,10 +1039,8 @@ export class ActivityPubServerService {
const { reject } = await this.checkAuthorizedFetch(request, reply);
if (reject) return;
const emoji = await this.emojisRepository.findOneBy({
host: IsNull(),
name: request.params.emoji,
});
const emojiKey = encodeEmojiKey({ name: request.params.emoji, host: null });
const emoji = await this.customEmojiService.emojisByKeyCache.fetchMaybe(emojiKey);
if (emoji == null || emoji.localOnly) {
reply.code(404);
@ -1048,7 +1048,7 @@ export class ActivityPubServerService {
}
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji)));
return (this.apRendererService.addContext(this.apRendererService.renderEmoji(emoji)));
});
// like

View file

@ -19,6 +19,7 @@ import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js';
import { genIdenticon } from '@/misc/gen-identicon.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { CustomEmojiService, encodeEmojiKey } from '@/core/CustomEmojiService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { renderInlineError } from '@/misc/render-inline-error.js';
@ -71,6 +72,7 @@ export class ServerService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
private oauth2ProviderService: OAuth2ProviderService,
private readonly customEmojiService: CustomEmojiService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
}
@ -171,14 +173,15 @@ export class ServerService implements OnApplicationShutdown {
return;
}
const name = pathChunks.shift();
const name = pathChunks.shift() as string;
const host = pathChunks.pop();
const emoji = await this.emojisRepository.findOneBy({
const emojiKey = encodeEmojiKey({
// `@.` is the spec of ReactionService.decodeReaction
host: (host === undefined || host === '.') ? IsNull() : host,
host: (host === undefined || host === '.') ? null : host,
name: name,
});
const emoji = await this.customEmojiService.emojisByKeyCache.fetchMaybe(emojiKey);
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');

View file

@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneBy({ id: ps.emojiId });
const emoji = await this.customEmojiService.emojisByIdCache.fetchMaybe(ps.emojiId);
if (emoji == null) {
throw new ApiError(meta.errors.noSuchEmoji);
}

View file

@ -98,6 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.query) {
// TODO use string inclusion func instead of dynamic query building
q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query.normalize('NFC')) + '%' })
.orderBy('length(emoji.name)', 'ASC');
}

View file

@ -9,6 +9,7 @@ import type { EmojisRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['meta'],
@ -47,14 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private readonly customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneOrFail({
where: {
name: ps.name,
host: IsNull(),
},
});
const emoji = await this.customEmojiService.emojisByKeyCache.fetch(ps.name);
return this.emojiEntityService.packDetailed(emoji);
});

View file

@ -4,10 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { EmojisRepository } from '@/models/_.js';
import type { EmojisRepository, MiEmoji } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { DI } from '@/di-symbols.js';
import { CacheManagementService, type ManagedMemorySingleCache } from '@/core/CacheManagementService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['meta'],
@ -32,8 +34,11 @@ export const meta = {
},
},
// 2 calls per second
// Up to 20 calls, then 5 / second
limit: {
type: 'bucket',
size: 20,
duration: 1000,
max: 2,
},
@ -48,21 +53,41 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
// Short (2 second) cache to handle rapid bursts of fetching the emoji list.
// This just stores the IDs - the actual emojis are cached by CustomEmojiService
private readonly localEmojiIdsCache: ManagedMemorySingleCache<MiEmoji['id'][]>;
constructor(
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private readonly customEmojiService: CustomEmojiService,
cacheManagementService: CacheManagementService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.createQueryBuilder()
.where('host IS NULL')
.orderBy('LOWER(category)', 'ASC')
.addOrderBy('LOWER(name)', 'ASC')
.getMany();
// Fetch the latest emoji list
const emojiIds = await this.localEmojiIdsCache.fetch(async () => {
const emojis = await this.emojisRepository.createQueryBuilder()
.select('id')
.where('host IS NULL')
.orderBy('LOWER(category)', 'ASC')
.addOrderBy('LOWER(name)', 'ASC')
.getMany() as { id: MiEmoji['id'] }[];
return emojis.map(e => e.id);
});
// Fetch the latest version of each emoji
const emojis = await this.customEmojiService.emojisByIdCache.fetchMany(emojiIds);
// Pack and return everything
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),
emojis: await this.emojiEntityService.packSimpleMany(emojis.values),
};
});
this.localEmojiIdsCache = cacheManagementService.createMemorySingleCache<MiEmoji['id'][]>(1000 * 2);
}
}