normalize cache constructors and detect duplicate cache names

This commit is contained in:
Hazelnoot 2025-11-02 11:58:28 -05:00
parent 151550602c
commit 8271dc199e
18 changed files with 95 additions and 61 deletions

View file

@ -36,7 +36,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
cacheManagementService: CacheManagementService,
) {
this.cache = cacheManagementService.createMemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s
this.cache = cacheManagementService.createMemorySingleCache<MiAvatarDecoration[]>('avatarDecorations', 1000 * 60 * 30); // 30s
this.internalEventService.on('avatarDecorationCreated', this.onAvatarEvent);
this.internalEventService.on('avatarDecorationUpdated', this.onAvatarEvent);

View file

@ -106,10 +106,10 @@ export class CacheService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = this.cacheManagementService.createMemoryKVCache<MiUser>(1000 * 60 * 5); // 5m
this.localUserByNativeTokenCache = this.cacheManagementService.createMemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m
this.localUserByIdCache = this.cacheManagementService.createMemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m
this.uriPersonCache = this.cacheManagementService.createMemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m
this.userByIdCache = this.cacheManagementService.createMemoryKVCache<MiUser>('userById', 1000 * 60 * 5); // 5m
this.localUserByNativeTokenCache = this.cacheManagementService.createMemoryKVCache<MiLocalUser | null>('localUserByNativeToken', 1000 * 60 * 5); // 5m
this.localUserByIdCache = this.cacheManagementService.createMemoryKVCache<MiLocalUser>('localUserById', 1000 * 60 * 5); // 5m
this.uriPersonCache = this.cacheManagementService.createMemoryKVCache<MiUser | null>('uriPerson', 1000 * 60 * 5); // 5m
this.userProfileCache = this.cacheManagementService.createQuantumKVCache('userProfile', {
lifetime: 1000 * 60 * 30, // 30m
@ -345,7 +345,7 @@ export class CacheService implements OnApplicationShutdown {
},
});
this.userFollowStatsCache = this.cacheManagementService.createMemoryKVCache<FollowStats>(1000 * 60 * 10); // 10 minutes
this.userFollowStatsCache = this.cacheManagementService.createMemoryKVCache<FollowStats>('followStats', 1000 * 60 * 10); // 10 minutes
this.userFollowingChannelsCache = this.cacheManagementService.createQuantumKVCache<Set<string>>('userFollowingChannels', {
lifetime: 1000 * 60 * 30, // 30m

View file

@ -56,10 +56,10 @@ export class InstanceStatsService {
cacheManagementService: CacheManagementService,
) {
this.localPostsCache = cacheManagementService.createMemorySingleCache<number>(1000 * 60 * 60); // 1h
this.localUsersCache = cacheManagementService.createMemorySingleCache<number>(1000 * 60 * 60); // 1h
this.activeMonthCache = cacheManagementService.createMemorySingleCache<number>(1000 * 60 * 60 * 24); // 1d
this.activeSixMonthsCache = cacheManagementService.createMemorySingleCache<number>(1000 * 60 * 60 * 24 * 7); // 1w
this.localPostsCache = cacheManagementService.createMemorySingleCache<number>('localPosts', 1000 * 60 * 60); // 1h
this.localUsersCache = cacheManagementService.createMemorySingleCache<number>('localUsers', 1000 * 60 * 60); // 1h
this.activeMonthCache = cacheManagementService.createMemorySingleCache<number>('activeMonth', 1000 * 60 * 60 * 24); // 1d
this.activeSixMonthsCache = cacheManagementService.createMemorySingleCache<number>('activeSixMonths', 1000 * 60 * 60 * 24 * 7); // 1w
}
@bindThis

View file

@ -32,7 +32,7 @@ export class RelayService {
cacheManagementService: CacheManagementService,
) {
this.relaysCache = cacheManagementService.createMemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
this.relaysCache = cacheManagementService.createMemorySingleCache<MiRelay[]>('relay', 1000 * 60 * 10); // 10m
}
@bindThis

View file

@ -162,8 +162,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
cacheManagementService: CacheManagementService,
) {
this.rolesCache = cacheManagementService.createMemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = cacheManagementService.createMemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
this.rolesCache = cacheManagementService.createMemorySingleCache<MiRole[]>('roles', 1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = cacheManagementService.createMemoryKVCache<MiRoleAssignment[]>('roleAssignment', 1000 * 60 * 5); // 5m
// TODO additional cache for final calculation?
this.redisForSub.on('message', this.onMessage);

View file

@ -56,7 +56,7 @@ export class SystemAccountService implements OnApplicationShutdown {
cacheManagementService: CacheManagementService,
) {
this.cache = cacheManagementService.createMemoryKVCache<string>(1000 * 60 * 10); // 10m
this.cache = cacheManagementService.createMemoryKVCache<string>('systemAccount', 1000 * 60 * 10); // 10m
this.redisForSub.on('message', this.onMessage);
}

View file

@ -69,7 +69,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
cacheManagementService: CacheManagementService,
) {
this.activeSystemWebhooks = cacheManagementService.createMemorySingleCache<MiSystemWebhook[]>(1000 * 60 * 60 * 12); // 12h
this.activeSystemWebhooks = cacheManagementService.createMemorySingleCache<MiSystemWebhook[]>('systemWebhooks', 1000 * 60 * 60 * 12); // 12h
this.internalEventService.on('systemWebhookCreated', this.onWebhookEvent);
this.internalEventService.on('systemWebhookUpdated', this.onWebhookEvent);

View file

@ -41,7 +41,7 @@ export class UserWebhookService implements OnApplicationShutdown {
cacheManagementService: CacheManagementService,
) {
this.activeWebhooks = cacheManagementService.createMemorySingleCache<MiWebhook[]>(1000 * 60 * 60 * 12); // 12h
this.activeWebhooks = cacheManagementService.createMemorySingleCache<MiWebhook[]>('userWebhooks', 1000 * 60 * 60 * 12); // 12h
this.internalEventService.on('webhookCreated', this.onWebhookEvent);
this.internalEventService.on('webhookUpdated', this.onWebhookEvent);

View file

@ -12,8 +12,15 @@ import {
RedisSingleCache,
type RedisKVCacheOpts,
type RedisSingleCacheOpts,
type MemoryCacheServices,
type RedisCacheServices,
type MemoryCacheOpts,
} from '@/misc/cache.js';
import { QuantumKVCache, type QuantumKVOpts } from '@/misc/QuantumKVCache.js';
import {
QuantumKVCache,
type QuantumKVOpts,
type QuantumCacheServices,
} from '@/misc/QuantumKVCache.js';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import { TimeService, type TimerHandle } from '@/global/TimeService.js';
@ -32,6 +39,8 @@ export type ManagedQuantumKVCache<T> = Managed<QuantumKVCache<T>>;
export type Managed<T> = Omit<T, 'dispose' | 'onApplicationShutdown' | 'gc'>;
export type Manager = { dispose(): void, clear(): void, gc(): void };
type CacheServices = MemoryCacheServices & RedisCacheServices & QuantumCacheServices;
export const GC_INTERVAL = 1000 * 60 * 3; // 3m
/**
@ -40,7 +49,7 @@ export const GC_INTERVAL = 1000 * 60 * 3; // 3m
*/
@Injectable()
export class CacheManagementService implements OnApplicationShutdown {
private readonly managedCaches = new Set<Manager>();
private readonly managedCaches = new Map<string, Manager>();
private gcTimer?: TimerHandle | null;
constructor(
@ -51,7 +60,7 @@ export class CacheManagementService implements OnApplicationShutdown {
private readonly internalEventService: InternalEventService,
) {}
private get cacheServices() {
private get cacheServices(): CacheServices {
return {
internalEventService: this.internalEventService,
redisClient: this.redisClient,
@ -60,52 +69,56 @@ export class CacheManagementService implements OnApplicationShutdown {
}
@bindThis
public createMemoryKVCache<T>(lifetime: number): ManagedMemoryKVCache<T> {
const cache = new MemoryKVCache<T>(lifetime, this.cacheServices);
return this.manageCache(cache);
public createMemoryKVCache<T>(name: string, optsOrLifetime: MemoryCacheOpts | number): ManagedMemoryKVCache<T> {
const opts = typeof(optsOrLifetime) === 'number' ? { lifetime: optsOrLifetime } : optsOrLifetime;
return this.create(name, () => new MemoryKVCache<T>(name, this.cacheServices, opts));
}
@bindThis
public createMemorySingleCache<T>(lifetime: number): ManagedMemorySingleCache<T> {
const cache = new MemorySingleCache<T>(lifetime, this.cacheServices);
return this.manageCache(cache);
public createMemorySingleCache<T>(name: string, optsOrLifetime: MemoryCacheOpts | number): ManagedMemorySingleCache<T> {
const opts = typeof(optsOrLifetime) === 'number' ? { lifetime: optsOrLifetime } : optsOrLifetime;
return this.create(name, () => new MemorySingleCache<T>(name, this.cacheServices, opts));
}
@bindThis
public createRedisKVCache<T>(name: string, opts: RedisKVCacheOpts<T>): ManagedRedisKVCache<T> {
const cache = new RedisKVCache<T>(name, this.cacheServices, opts);
return this.manageCache(cache);
return this.create(name, () => new RedisKVCache<T>(name, this.cacheServices, opts));
}
@bindThis
public createRedisSingleCache<T>(name: string, opts: RedisSingleCacheOpts<T>): ManagedRedisSingleCache<T> {
const cache = new RedisSingleCache<T>(name, this.cacheServices, opts);
return this.manageCache(cache);
return this.create(name, () => new RedisSingleCache<T>(name, this.cacheServices, opts));
}
@bindThis
public createQuantumKVCache<T>(name: string, opts: QuantumKVOpts<T>): ManagedQuantumKVCache<T> {
const cache = new QuantumKVCache<T>(name, this.cacheServices, opts);
return this.manageCache(cache);
return this.create(name, () => new QuantumKVCache<T>(name, this.cacheServices, opts));
}
protected manageCache<T extends Manager>(cache: T): Managed<T> {
this.managedCaches.add(cache);
private create<T extends Manager>(name: string, factory: () => T): T {
if (this.managedCaches.has(name)) {
throw new Error(`Duplicate cache name: "${name}"`);
}
const cache = factory();
this.managedCaches.set(name, cache);
this.startGcTimer();
return cache;
}
@bindThis
public gc(): void {
this.resetGcTimer(() => {
callAllOn(this.managedCaches, 'gc');
callAllOn(this.managedCaches.values(), 'gc');
});
}
@bindThis
public clear(): void {
this.resetGcTimer(() => {
callAllOn(this.managedCaches, 'clear');
callAllOn(this.managedCaches.values(), 'clear');
});
}
@ -113,7 +126,7 @@ export class CacheManagementService implements OnApplicationShutdown {
public async dispose(): Promise<void> {
this.stopGcTimer();
const toDispose = new Set(this.managedCaches);
const toDispose = Array.from(this.managedCaches.values());
this.managedCaches.clear();
callAllOn(toDispose, 'dispose');

View file

@ -14,7 +14,7 @@ export class DependencyService {
protected readonly dependencyVersionCache: ManagedMemoryKVCache<string | null>;
constructor(cacheManagementService: CacheManagementService) {
this.dependencyVersionCache = cacheManagementService.createMemoryKVCache<string | null>(Infinity);
this.dependencyVersionCache = cacheManagementService.createMemoryKVCache<string | null>('dependencyVersion', Infinity);
}
/**

View file

@ -79,7 +79,7 @@ export class QuantumKVCache<T> implements Iterable<readonly [key: string, value:
) {
// OK: we forward all management calls to the inner cache.
// eslint-disable-next-line no-restricted-syntax
this.memoryCache = new MemoryKVCache(opts.lifetime, services);
this.memoryCache = new MemoryKVCache(name + ':mem', services, { lifetime: opts.lifetime });
this.fetcher = opts.fetcher;
this.bulkFetcher = opts.bulkFetcher;
this.onChanged = opts.onChanged;

View file

@ -36,7 +36,7 @@ export class RedisKVCache<T> {
this.lifetime = opts.lifetime;
// OK: we forward all management calls to the inner cache.
// eslint-disable-next-line no-restricted-syntax
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime, services);
this.memoryCache = new MemoryKVCache(name + ':mem', services, { lifetime: opts.memoryCacheLifetime });
this.fetcher = opts.fetcher ?? (() => { throw new Error('fetch not supported - use get/set directly'); });
this.toRedisConverter = opts.toRedisConverter ?? ((value) => JSON.stringify(value));
this.fromRedisConverter = opts.fromRedisConverter ?? ((value) => JSON.parse(value));
@ -151,7 +151,7 @@ export class RedisSingleCache<T> {
this.lifetime = opts.lifetime;
// OK: we forward all management calls to the inner cache.
// eslint-disable-next-line no-restricted-syntax
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime, services);
this.memoryCache = new MemorySingleCache(name + ':mem', services, { lifetime: opts.memoryCacheLifetime });
this.fetcher = opts.fetcher ?? (() => { throw new Error('fetch not supported - use get/set directly'); });
this.toRedisConverter = opts.toRedisConverter ?? ((value) => JSON.stringify(value));
@ -247,17 +247,24 @@ export interface MemoryCacheServices {
readonly timeService: TimeService;
}
export interface MemoryCacheOpts {
lifetime: number;
}
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class MemoryKVCache<T> {
private readonly cache = new Map<string, { date: number; value: T; }>();
private readonly timeService: TimeService;
private readonly lifetime: number;
constructor(
private readonly lifetime: number,
public readonly name: string,
services: MemoryCacheServices,
opts: MemoryCacheOpts,
) {
this.timeService = services.timeService;
this.lifetime = opts.lifetime;
}
@bindThis
@ -388,14 +395,18 @@ export class MemoryKVCache<T> {
export class MemorySingleCache<T> {
private readonly timeService: TimeService;
private readonly lifetime: number;
private cachedAt: number | null = null;
private value: T | undefined;
constructor(
private lifetime: number,
public readonly name: string,
services: MemoryCacheServices,
opts: MemoryCacheOpts,
) {
this.timeService = services.timeService;
this.lifetime = opts.lifetime;
}
@bindThis

View file

@ -46,8 +46,8 @@ export class SkRateLimiterService {
envService: EnvService,
cacheManagementService: CacheManagementService,
) {
this.factorCache = cacheManagementService.createMemoryKVCache<number>(1000 * 60); // 1m
this.lockoutCache = cacheManagementService.createMemoryKVCache<number>(1000 * 10); // 10s
this.factorCache = cacheManagementService.createMemoryKVCache<number>('rateLimitFactor', 1000 * 60); // 1m
this.lockoutCache = cacheManagementService.createMemoryKVCache<number>('rateLimitLockout', 1000 * 10); // 10s
this.disabled = envService.env.NODE_ENV === 'test';
}

View file

@ -43,7 +43,7 @@ export class AuthenticateService {
cacheManagementService: CacheManagementService,
) {
this.appCache = cacheManagementService.createMemoryKVCache<MiApp>(1000 * 60 * 60 * 24); // 1d
this.appCache = cacheManagementService.createMemoryKVCache<MiApp>('app', 1000 * 60 * 60 * 24); // 1d
}
@bindThis

View file

@ -87,6 +87,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
};
});
this.localEmojiIdsCache = cacheManagementService.createMemorySingleCache<MiEmoji['id'][]>(1000 * 2);
this.localEmojiIdsCache = cacheManagementService.createMemorySingleCache<MiEmoji['id'][]>('localEmojis', 1000 * 2);
}
}

View file

@ -9,7 +9,7 @@ import { GodOfTimeService } from './GodOfTimeService.js';
import { MockInternalEventService } from './MockInternalEventService.js';
import { MockRedis } from './MockRedis.js';
import type { QuantumKVOpts } from '@/misc/QuantumKVCache.js';
import type { RedisKVCacheOpts, RedisSingleCacheOpts } from '@/misc/cache.js';
import type { RedisKVCacheOpts, RedisSingleCacheOpts, MemoryCacheOpts } from '@/misc/cache.js';
import type { TimeService } from '@/global/TimeService.js';
import type { InternalEventService } from '@/global/InternalEventService.js';
import {
@ -40,12 +40,14 @@ export class FakeCacheManagementService extends CacheManagementService {
super(redisClient, timeService, internalEventService);
}
createMemoryKVCache<T>(): ManagedMemoryKVCache<T> {
return super.createMemoryKVCache(-1);
createMemoryKVCache<T>(name: string, optsOrLifetime: number | MemoryCacheOpts): ManagedMemoryKVCache<T> {
const opts = typeof(optsOrLifetime) === 'number' ? { lifetime: -1 } : { ...optsOrLifetime, lifetime: -1 };
return super.createMemoryKVCache(name, opts);
}
createMemorySingleCache<T>(): ManagedMemorySingleCache<T> {
return super.createMemorySingleCache(-1);
createMemorySingleCache<T>(name: string, optsOrLifetime: number | MemoryCacheOpts): ManagedMemorySingleCache<T> {
const opts = typeof(optsOrLifetime) === 'number' ? { lifetime: -1 } : { ...optsOrLifetime, lifetime: -1 };
return super.createMemorySingleCache(name, opts);
}
createRedisKVCache<T>(name: string, opts: RedisKVCacheOpts<T>): ManagedRedisKVCache<T> {

View file

@ -44,14 +44,14 @@ describe(CacheManagementService, () => {
function createCache(): MemoryKVCache<string> {
// Cast to allow access to managed functions, for spying purposes.
return serviceUnderTest.createMemoryKVCache<string>(Infinity) as MemoryKVCache<string>;
return serviceUnderTest.createMemoryKVCache<string>('test', Infinity) as MemoryKVCache<string>;
}
describe('createMemoryKVCache', () => testCreate('createMemoryKVCache', Infinity));
describe('createMemorySingleCache', () => testCreate('createMemorySingleCache', Infinity));
describe('createRedisKVCache', () => testCreate('createRedisKVCache', 'redis', { lifetime: Infinity, memoryCacheLifetime: Infinity }));
describe('createRedisSingleCache', () => testCreate('createRedisSingleCache', 'single', { lifetime: Infinity, memoryCacheLifetime: Infinity }));
describe('createQuantumKVCache', () => testCreate('createQuantumKVCache', 'quantum', { lifetime: Infinity, fetcher: () => { throw new Error('not implement'); } }));
describe('createMemoryKVCache', () => testCreate('createMemoryKVCache', 'memoryKV', { lifetime: Infinity }));
describe('createMemorySingleCache', () => testCreate('createMemorySingleCache', 'memorySingle', { lifetime: Infinity }));
describe('createRedisKVCache', () => testCreate('createRedisKVCache', 'redisKV', { lifetime: Infinity, memoryCacheLifetime: Infinity }));
describe('createRedisSingleCache', () => testCreate('createRedisSingleCache', 'redisSingle', { lifetime: Infinity, memoryCacheLifetime: Infinity }));
describe('createQuantumKVCache', () => testCreate('createQuantumKVCache', 'quantumKV', { lifetime: Infinity, fetcher: () => { throw new Error('not implement'); } }));
describe('clear', () => {
testClear('clear', false);
@ -80,7 +80,7 @@ describe(CacheManagementService, () => {
it('should track reference', () => {
const cache = act();
expect(internalsUnderTest.managedCaches).toContain(cache);
expect(internalsUnderTest.managedCaches.values()).toContain(cache);
});
it('should start GC timer', () => {
@ -91,6 +91,12 @@ describe(CacheManagementService, () => {
expect(gc).toHaveBeenCalledTimes(3);
});
it('should throw if name is duplicate', () => {
act();
expect(() => act()).toThrow();
});
}
function testClear(func: 'clear' | 'dispose' | 'onApplicationShutdown', shouldDispose: boolean) {
@ -140,9 +146,9 @@ describe(CacheManagementService, () => {
act();
if (shouldDispose) {
expect(internalsUnderTest.managedCaches).not.toContain(cache);
expect(internalsUnderTest.managedCaches.values()).not.toContain(cache);
} else {
expect(internalsUnderTest.managedCaches).toContain(cache);
expect(internalsUnderTest.managedCaches.values()).toContain(cache);
}
});

View file

@ -6,7 +6,9 @@
import { jest } from '@jest/globals';
import { GodOfTimeService } from '../../misc/GodOfTimeService.js';
import { MockInternalEventService } from '../../misc/MockInternalEventService.js';
import { QuantumKVCache, QuantumKVOpts, FetchFailedError, KeyNotFoundError } from '@/misc/QuantumKVCache.js';
import { QuantumKVCache, QuantumKVOpts } from '@/misc/QuantumKVCache.js';
import { KeyNotFoundError } from '@/misc/errors/KeyNotFoundError.js';
import { FetchFailedError } from '@/misc/errors/FetchFailedError.js';
describe(QuantumKVCache, () => {
let fakeTimeService: GodOfTimeService;