diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-06-06 12:17:04 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-06-09 11:02:36 -0400 |
| commit | 2e486f02ffb83192fd46fd231fb3604e1b982416 (patch) | |
| tree | fe351c8793f32e06c55e25c6f957e5decd83a9c8 /packages | |
| parent | fix reversed isBlocking / isBlocked in UserEntityService.getRelation (diff) | |
| download | sharkey-2e486f02ffb83192fd46fd231fb3604e1b982416.tar.gz sharkey-2e486f02ffb83192fd46fd231fb3604e1b982416.tar.bz2 sharkey-2e486f02ffb83192fd46fd231fb3604e1b982416.zip | |
implement no-op caches for testing
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/backend/src/core/CacheService.ts | 22 | ||||
| -rw-r--r-- | packages/backend/src/misc/cache.ts | 41 | ||||
| -rw-r--r-- | packages/backend/test/misc/noOpCaches.ts | 196 |
3 files changed, 244 insertions, 15 deletions
diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index ae24a9721f..2c136eac2b 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -28,7 +28,7 @@ export interface CachedTranslation { text: string | undefined; } -interface CachedTranslationEntity { +export interface CachedTranslationEntity { l?: string; t?: string; u?: number; @@ -46,8 +46,8 @@ export class CacheService implements OnApplicationShutdown { public userBlockedCache: QuantumKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ public renoteMutingsCache: QuantumKVCache<Set<string>>; public userFollowingsCache: QuantumKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>; - private readonly userFollowStatsCache = new MemoryKVCache<FollowStats>(1000 * 60 * 10); // 10 minutes - private readonly translationsCache: RedisKVCache<CachedTranslationEntity>; + protected userFollowStatsCache = new MemoryKVCache<FollowStats>(1000 * 60 * 10); // 10 minutes + protected translationsCache: RedisKVCache<CachedTranslationEntity>; constructor( @Inject(DI.redis) @@ -468,6 +468,22 @@ export class CacheService implements OnApplicationShutdown { } @bindThis + public clear(): void { + this.userByIdCache.clear(); + this.localUserByNativeTokenCache.clear(); + this.localUserByIdCache.clear(); + this.uriPersonCache.clear(); + this.userProfileCache.clear(); + this.userMutingsCache.clear(); + this.userBlockingCache.clear(); + this.userBlockedCache.clear(); + this.renoteMutingsCache.clear(); + this.userFollowingsCache.clear(); + this.userFollowStatsCache.clear(); + this.translationsCache.clear(); + } + + @bindThis public dispose(): void { this.internalEventService.off('userChangeSuspendedState', this.onUserEvent); this.internalEventService.off('userChangeDeletedState', this.onUserEvent); diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index 3145550a44..0a1cf6adb4 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -11,9 +11,9 @@ import { InternalEventTypes } from '@/core/GlobalEventService.js'; export class RedisKVCache<T> { private readonly lifetime: number; private readonly memoryCache: MemoryKVCache<T>; - private readonly fetcher: (key: string) => Promise<T>; - private readonly toRedisConverter: (value: T) => string; - private readonly fromRedisConverter: (value: string) => T | undefined; + public readonly fetcher: (key: string) => Promise<T>; + public readonly toRedisConverter: (value: T) => string; + public readonly fromRedisConverter: (value: string) => T | undefined; constructor( private redisClient: Redis.Redis, @@ -102,6 +102,11 @@ export class RedisKVCache<T> { } @bindThis + public clear() { + this.memoryCache.clear(); + } + + @bindThis public gc() { this.memoryCache.gc(); } @@ -125,16 +130,17 @@ export class RedisSingleCache<T> { opts: { lifetime: number; memoryCacheLifetime: number; - fetcher: RedisSingleCache<T>['fetcher']; - toRedisConverter: RedisSingleCache<T>['toRedisConverter']; - fromRedisConverter: RedisSingleCache<T>['fromRedisConverter']; + fetcher?: RedisSingleCache<T>['fetcher']; + toRedisConverter?: RedisSingleCache<T>['toRedisConverter']; + fromRedisConverter?: RedisSingleCache<T>['fromRedisConverter']; }, ) { this.lifetime = opts.lifetime; this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime); - this.fetcher = opts.fetcher; - this.toRedisConverter = opts.toRedisConverter; - this.fromRedisConverter = opts.fromRedisConverter; + + 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)); } @bindThis @@ -417,6 +423,8 @@ export class MemorySingleCache<T> { } } +// TODO move to separate file + export interface QuantumKVOpts<T> { /** * Memory cache lifetime in milliseconds. @@ -452,9 +460,9 @@ export interface QuantumKVOpts<T> { export class QuantumKVCache<T> implements Iterable<[key: string, value: T]> { private readonly memoryCache: MemoryKVCache<T>; - private readonly fetcher: QuantumKVOpts<T>['fetcher']; - private readonly onSet: QuantumKVOpts<T>['onSet']; - private readonly onDelete: QuantumKVOpts<T>['onDelete']; + public readonly fetcher: QuantumKVOpts<T>['fetcher']; + public readonly onSet: QuantumKVOpts<T>['onSet']; + public readonly onDelete: QuantumKVOpts<T>['onDelete']; /** * @param internalEventService Service bus to synchronize events. @@ -676,6 +684,15 @@ export class QuantumKVCache<T> implements Iterable<[key: string, value: T]> { * Does not send any events or update other processes. */ @bindThis + public clear() { + this.memoryCache.clear(); + } + + /** + * Removes expired cache entries from the local view. + * Does not send any events or update other processes. + */ + @bindThis public gc() { this.memoryCache.gc(); } diff --git a/packages/backend/test/misc/noOpCaches.ts b/packages/backend/test/misc/noOpCaches.ts new file mode 100644 index 0000000000..373c7bddcc --- /dev/null +++ b/packages/backend/test/misc/noOpCaches.ts @@ -0,0 +1,196 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Redis from 'ioredis'; +import { Inject } from '@nestjs/common'; +import { FakeInternalEventService } from './FakeInternalEventService.js'; +import type { BlockingsRepository, FollowingsRepository, MiFollowing, MiUser, MiUserProfile, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { MiLocalUser } from '@/models/User.js'; +import { MemoryKVCache, MemorySingleCache, QuantumKVCache, QuantumKVOpts, RedisKVCache, RedisSingleCache } from '@/misc/cache.js'; +import { CacheService, CachedTranslationEntity, FollowStats } from '@/core/CacheService.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; + +export function noOpRedis() { + return { + set: () => Promise.resolve(), + get: () => Promise.resolve(null), + del: () => Promise.resolve(), + on: () => {}, + off: () => {}, + } as unknown as Redis.Redis; +} + +export class NoOpCacheService extends CacheService { + public readonly fakeRedis: { + [K in keyof Redis.Redis]: Redis.Redis[K]; + }; + public readonly fakeInternalEventService: FakeInternalEventService; + + constructor( + @Inject(DI.usersRepository) + usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + userProfilesRepository: UserProfilesRepository, + + @Inject(DI.mutingsRepository) + mutingsRepository: MutingsRepository, + + @Inject(DI.blockingsRepository) + blockingsRepository: BlockingsRepository, + + @Inject(DI.renoteMutingsRepository) + renoteMutingsRepository: RenoteMutingsRepository, + + @Inject(DI.followingsRepository) + followingsRepository: FollowingsRepository, + + @Inject(UserEntityService) + userEntityService: UserEntityService, + ) { + const fakeRedis = noOpRedis(); + const fakeInternalEventService = new FakeInternalEventService(); + + super( + fakeRedis, + fakeRedis, + usersRepository, + userProfilesRepository, + mutingsRepository, + blockingsRepository, + renoteMutingsRepository, + followingsRepository, + userEntityService, + fakeInternalEventService, + ); + + this.fakeRedis = fakeRedis; + this.fakeInternalEventService = fakeInternalEventService; + + // Override caches + this.userByIdCache = new NoOpMemoryKVCache<MiUser>(); + this.localUserByNativeTokenCache = new NoOpMemoryKVCache<MiLocalUser | null>(); + this.localUserByIdCache = new NoOpMemoryKVCache<MiLocalUser>(); + this.uriPersonCache = new NoOpMemoryKVCache<MiUser | null>(); + this.userProfileCache = new NoOpQuantumKVCache<MiUserProfile>({ + internalEventService: fakeInternalEventService, + fetcher: this.userProfileCache.fetcher, + onSet: this.userProfileCache.onSet, + onDelete: this.userProfileCache.onDelete, + }); + this.userMutingsCache = new NoOpQuantumKVCache<Set<string>>({ + internalEventService: fakeInternalEventService, + fetcher: this.userMutingsCache.fetcher, + onSet: this.userMutingsCache.onSet, + onDelete: this.userMutingsCache.onDelete, + }); + this.userBlockingCache = new NoOpQuantumKVCache<Set<string>>({ + internalEventService: fakeInternalEventService, + fetcher: this.userBlockingCache.fetcher, + onSet: this.userBlockingCache.onSet, + onDelete: this.userBlockingCache.onDelete, + }); + this.userBlockedCache = new NoOpQuantumKVCache<Set<string>>({ + internalEventService: fakeInternalEventService, + fetcher: this.userBlockedCache.fetcher, + onSet: this.userBlockedCache.onSet, + onDelete: this.userBlockedCache.onDelete, + }); + this.renoteMutingsCache = new NoOpQuantumKVCache<Set<string>>({ + internalEventService: fakeInternalEventService, + fetcher: this.renoteMutingsCache.fetcher, + onSet: this.renoteMutingsCache.onSet, + onDelete: this.renoteMutingsCache.onDelete, + }); + this.userFollowingsCache = new NoOpQuantumKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>({ + internalEventService: fakeInternalEventService, + fetcher: this.userFollowingsCache.fetcher, + onSet: this.userFollowingsCache.onSet, + onDelete: this.userFollowingsCache.onDelete, + }); + this.userFollowStatsCache = new NoOpMemoryKVCache<FollowStats>(); + this.translationsCache = new NoOpRedisKVCache<CachedTranslationEntity>({ + redis: fakeRedis, + fetcher: this.translationsCache.fetcher, + toRedisConverter: this.translationsCache.toRedisConverter, + fromRedisConverter: this.translationsCache.fromRedisConverter, + }); + } +} + +export class NoOpMemoryKVCache<T> extends MemoryKVCache<T> { + constructor() { + super(-1); + } +} + +export class NoOpMemorySingleCache<T> extends MemorySingleCache<T> { + constructor() { + super(-1); + } +} + +export class NoOpRedisKVCache<T> extends RedisKVCache<T> { + constructor(opts?: { + redis?: Redis.Redis; + fetcher?: RedisKVCache<T>['fetcher']; + toRedisConverter?: RedisKVCache<T>['toRedisConverter']; + fromRedisConverter?: RedisKVCache<T>['fromRedisConverter']; + }) { + super( + opts?.redis ?? noOpRedis(), + 'no-op', + { + lifetime: -1, + memoryCacheLifetime: -1, + fetcher: opts?.fetcher, + toRedisConverter: opts?.toRedisConverter, + fromRedisConverter: opts?.fromRedisConverter, + }, + ); + } +} + +export class NoOpRedisSingleCache<T> extends RedisSingleCache<T> { + constructor(opts?: { + fakeRedis?: Redis.Redis; + fetcher?: RedisSingleCache<T>['fetcher']; + toRedisConverter?: RedisSingleCache<T>['toRedisConverter']; + fromRedisConverter?: RedisSingleCache<T>['fromRedisConverter']; + }) { + super( + opts?.fakeRedis ?? noOpRedis(), + 'no-op', + { + lifetime: -1, + memoryCacheLifetime: -1, + fetcher: opts?.fetcher, + toRedisConverter: opts?.toRedisConverter, + fromRedisConverter: opts?.fromRedisConverter, + }, + ); + } +} + +export class NoOpQuantumKVCache<T> extends QuantumKVCache<T> { + constructor(opts: { + internalEventService?: FakeInternalEventService, + fetcher: QuantumKVOpts<T>['fetcher'], + onSet?: QuantumKVOpts<T>['onSet'], + onDelete?: QuantumKVOpts<T>['onDelete'], + }) { + super( + opts.internalEventService ?? new FakeInternalEventService(), + 'no-op', + { + lifetime: -1, + fetcher: opts.fetcher, + onSet: opts.onSet, + onDelete: opts.onDelete, + }, + ); + } +} |