From 39286f6fcf067bcaf7681930b9452b3f3d73a2af Mon Sep 17 00:00:00 2001 From: tebrihk Date: Tue, 2 Jun 2026 13:57:37 +0100 Subject: [PATCH] solved --- .../modules/cache/cache-strategy.service.ts | 96 ++++++++++++++++--- .../modules/cache/cache-warming.service.ts | 92 ++++++++++++++++++ backend/src/modules/cache/cache.controller.ts | 41 +++++++- backend/src/modules/cache/cache.module.ts | 7 +- 4 files changed, 218 insertions(+), 18 deletions(-) create mode 100644 backend/src/modules/cache/cache-warming.service.ts diff --git a/backend/src/modules/cache/cache-strategy.service.ts b/backend/src/modules/cache/cache-strategy.service.ts index f071f67ef..be9c720c1 100644 --- a/backend/src/modules/cache/cache-strategy.service.ts +++ b/backend/src/modules/cache/cache-strategy.service.ts @@ -14,18 +14,26 @@ interface CacheMetrics { misses: number; sets: number; deletes: number; + keyMetrics: Map; } @Injectable() export class CacheStrategyService { private readonly logger = new Logger(CacheStrategyService.name); - private metrics: CacheMetrics = { hits: 0, misses: 0, sets: 0, deletes: 0 }; + private metrics: CacheMetrics = { + hits: 0, + misses: 0, + sets: 0, + deletes: 0, + keyMetrics: new Map() + }; private resourceTTLs = new Map([ ['user', 5 * 60 * 1000], // 5 minutes ['savings', 10 * 60 * 1000], // 10 minutes ['analytics', 30 * 60 * 1000], // 30 minutes ['blockchain', 2 * 60 * 1000], // 2 minutes ]); + private tagKeys = new Map>(); constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} @@ -34,9 +42,11 @@ export class CacheStrategyService { const value = await this.cacheManager.get(key); if (value) { this.metrics.hits++; + this.updateKeyMetrics(key, 'hits'); this.logger.debug(`Cache hit: ${key}`); } else { this.metrics.misses++; + this.updateKeyMetrics(key, 'misses'); } return value; } catch (error) { @@ -45,11 +55,22 @@ export class CacheStrategyService { } } - async set(key: string, value: T, ttl?: number): Promise { + async set(key: string, value: T, ttl?: number, tags?: string[]): Promise { try { const finalTTL = ttl || this.getDefaultTTL(key); await this.cacheManager.set(key, value, finalTTL); this.metrics.sets++; + this.updateKeyMetrics(key, 'sets'); + + if (tags) { + for (const tag of tags) { + if (!this.tagKeys.has(tag)) { + this.tagKeys.set(tag, new Set()); + } + this.tagKeys.get(tag)!.add(key); + } + } + this.logger.debug(`Cache set: ${key} (TTL: ${finalTTL}ms)`); } catch (error) { this.logger.error(`Cache set error for key ${key}:`, error); @@ -60,6 +81,12 @@ export class CacheStrategyService { try { await this.cacheManager.del(key); this.metrics.deletes++; + + // Remove key from all tag sets + for (const [, keys] of this.tagKeys) { + keys.delete(key); + } + this.logger.debug(`Cache deleted: ${key}`); } catch (error) { this.logger.error(`Cache delete error for key ${key}:`, error); @@ -68,29 +95,52 @@ export class CacheStrategyService { async invalidateByTag(tag: string): Promise { try { - const keys = Array.from(this.cacheManager.stores.keys()); - const keysToDelete = keys.filter((k) => k.toString().includes(tag)); - + const keysToDelete = this.tagKeys.get(tag) || new Set(); + for (const key of keysToDelete) { - await this.del(key.toString()); + await this.del(key); } - + + this.tagKeys.delete(tag); + this.logger.debug( - `Invalidated ${keysToDelete.length} keys with tag: ${tag}`, + `Invalidated ${keysToDelete.size} keys with tag: ${tag}`, ); } catch (error) { this.logger.error(`Cache invalidation error for tag ${tag}:`, error); } } + async invalidateByPattern(pattern: string): Promise { + try { + // This is a fallback for implementations that don't support pattern matching + // For Redis, we'd use KEYS or SCAN + const allKeys = Array.from(this.tagKeys.values()).flatMap(set => Array.from(set)); + const uniqueKeys = new Set(allKeys); + + const keysToDelete = Array.from(uniqueKeys).filter(k => k.includes(pattern)); + + for (const key of keysToDelete) { + await this.del(key); + } + + this.logger.debug( + `Invalidated ${keysToDelete.length} keys with pattern: ${pattern}`, + ); + } catch (error) { + this.logger.error(`Cache invalidation error for pattern ${pattern}:`, error); + } + } + async warmCache( key: string, loader: () => Promise, ttl?: number, + tags?: string[], ): Promise { try { const data = await loader(); - await this.set(key, data, ttl); + await this.set(key, data, ttl, tags); this.logger.log(`Cache warmed: ${key}`); } catch (error) { this.logger.error(`Cache warming error for key ${key}:`, error); @@ -101,12 +151,13 @@ export class CacheStrategyService { key: string, loader: () => Promise, ttl?: number, + tags?: string[], ): Promise { const cached = await this.get(key); if (cached) return cached; const data = await loader(); - await this.set(key, data, ttl); + await this.set(key, data, ttl, tags); return data; } @@ -115,26 +166,35 @@ export class CacheStrategyService { loader: () => Promise, ttl: number, staleTime: number, + tags?: string[], ): Promise { const cached = await this.get(key); if (cached) return cached; const data = await loader(); - await this.set(key, data, ttl + staleTime); + await this.set(key, data, ttl + staleTime, tags); return data; } getMetrics() { const total = this.metrics.hits + this.metrics.misses; + const keyMetricsArray = Array.from(this.metrics.keyMetrics.entries()).map(([key, km]) => ({ + key, + ...km, + hitRate: (km.hits + km.misses) > 0 + ? ((km.hits / (km.hits + km.misses)) * 100).toFixed(2) + '%' + : '0%', + })); + return { ...this.metrics, - hitRate: - total > 0 ? ((this.metrics.hits / total) * 100).toFixed(2) + '%' : '0%', + keyMetrics: keyMetricsArray, + hitRate: total > 0 ? ((this.metrics.hits / total) * 100).toFixed(2) + '%' : '0%', }; } resetMetrics() { - this.metrics = { hits: 0, misses: 0, sets: 0, deletes: 0 }; + this.metrics = { hits: 0, misses: 0, sets: 0, deletes: 0, keyMetrics: new Map() }; } setResourceTTL(resource: string, ttl: number): void { @@ -149,4 +209,12 @@ export class CacheStrategyService { } return 5 * 60 * 1000; // default 5 minutes } + + private updateKeyMetrics(key: string, type: 'hits' | 'misses' | 'sets') { + if (!this.metrics.keyMetrics.has(key)) { + this.metrics.keyMetrics.set(key, { hits: 0, misses: 0, sets: 0 }); + } + const km = this.metrics.keyMetrics.get(key)!; + km[type]++; + } } diff --git a/backend/src/modules/cache/cache-warming.service.ts b/backend/src/modules/cache/cache-warming.service.ts new file mode 100644 index 000000000..8896e7f4b --- /dev/null +++ b/backend/src/modules/cache/cache-warming.service.ts @@ -0,0 +1,92 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { CacheStrategyService } from './cache-strategy.service'; +import { Cron, CronExpression } from '@nestjs/schedule'; + +export enum CachePriority { + LOW = 1, + MEDIUM = 2, + HIGH = 3, + CRITICAL = 4, +} + +export interface CacheableEndpoint { + key: string; + priority: CachePriority; + loader: () => Promise; + ttl?: number; +} + +@Injectable() +export class CacheWarmingService { + private readonly logger = new Logger(CacheWarmingService.name); + private cacheableEndpoints: CacheableEndpoint[] = []; + private warmingMetrics = { + totalWarmed: 0, + successCount: 0, + failureCount: 0, + lastWarmedAt: null as Date | null, + warmingDuration: 0, + }; + + constructor(private readonly cacheStrategy: CacheStrategyService) {} + + registerCacheableEndpoint(endpoint: CacheableEndpoint): void { + this.cacheableEndpoints.push(endpoint); + this.logger.log(`Registered cacheable endpoint: ${endpoint.key}`); + } + + async warmAllEndpoints(): Promise { + const startTime = Date.now(); + this.logger.log('Starting cache warming...'); + + try { + const sortedEndpoints = [...this.cacheableEndpoints].sort( + (a, b) => b.priority - a.priority, + ); + + for (const endpoint of sortedEndpoints) { + await this.warmEndpoint(endpoint); + } + + const duration = Date.now() - startTime; + this.warmingMetrics.warmingDuration = duration; + this.warmingMetrics.lastWarmedAt = new Date(); + this.warmingMetrics.totalWarmed += sortedEndpoints.length; + this.logger.log(`Cache warming completed in ${duration}ms`); + } catch (error) { + this.logger.error('Cache warming failed:', error); + } + } + + async warmEndpoint(endpoint: CacheableEndpoint): Promise { + try { + await this.cacheStrategy.warmCache( + endpoint.key, + endpoint.loader, + endpoint.ttl, + ); + this.warmingMetrics.successCount++; + } catch (error) { + this.warmingMetrics.failureCount++; + this.logger.error(`Failed to warm endpoint ${endpoint.key}:`, error); + } + } + + @Cron(CronExpression.EVERY_DAY_AT_2AM) + handleScheduledWarmup(): void { + this.warmAllEndpoints(); + } + + getWarmingMetrics() { + return { + ...this.warmingMetrics, + successRate: this.warmingMetrics.totalWarmed > 0 + ? ((this.warmingMetrics.successCount / this.warmingMetrics.totalWarmed) * 100).toFixed(2) + '%' + : '0%', + }; + } + + getRegisteredEndpoints(): CacheableEndpoint[] { + return this.cacheableEndpoints; + } +} diff --git a/backend/src/modules/cache/cache.controller.ts b/backend/src/modules/cache/cache.controller.ts index f90b818e6..e413916fb 100644 --- a/backend/src/modules/cache/cache.controller.ts +++ b/backend/src/modules/cache/cache.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Controller, Get, Post, Delete, UseGuards, Param } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { CacheStrategyService } from './cache-strategy.service'; +import { CacheWarmingService } from './cache-warming.service'; import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; @ApiTags('Cache') @@ -8,7 +9,10 @@ import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() export class CacheController { - constructor(private readonly cacheStrategy: CacheStrategyService) {} + constructor( + private readonly cacheStrategy: CacheStrategyService, + private readonly cacheWarming: CacheWarmingService, + ) {} @Get('metrics') @ApiOperation({ summary: 'Get cache hit/miss metrics' }) @@ -22,4 +26,37 @@ export class CacheController { this.cacheStrategy.resetMetrics(); return { message: 'Cache metrics reset' }; } + + @Get('warming-metrics') + @ApiOperation({ summary: 'Get cache warming metrics' }) + getWarmingMetrics() { + return this.cacheWarming.getWarmingMetrics(); + } + + @Get('registered-endpoints') + @ApiOperation({ summary: 'Get registered cacheable endpoints' }) + getRegisteredEndpoints() { + return this.cacheWarming.getRegisteredEndpoints(); + } + + @Post('warm-all') + @ApiOperation({ summary: 'Warm all cacheable endpoints manually' }) + async warmAllEndpoints() { + await this.cacheWarming.warmAllEndpoints(); + return { message: 'Cache warming initiated' }; + } + + @Delete('invalidate/tag/:tag') + @ApiOperation({ summary: 'Invalidate all cache entries with the given tag' }) + async invalidateByTag(@Param('tag') tag: string) { + await this.cacheStrategy.invalidateByTag(tag); + return { message: `Invalidated all keys with tag: ${tag}` }; + } + + @Delete('invalidate/pattern/:pattern') + @ApiOperation({ summary: 'Invalidate all cache entries matching the given pattern' }) + async invalidateByPattern(@Param('pattern') pattern: string) { + await this.cacheStrategy.invalidateByPattern(pattern); + return { message: `Invalidated all keys matching pattern: ${pattern}` }; + } } diff --git a/backend/src/modules/cache/cache.module.ts b/backend/src/modules/cache/cache.module.ts index bfa646e33..2541455bd 100644 --- a/backend/src/modules/cache/cache.module.ts +++ b/backend/src/modules/cache/cache.module.ts @@ -4,9 +4,12 @@ import * as redisStore from 'cache-manager-redis-store'; import { ConfigService } from '@nestjs/config'; import { CacheStrategyService } from './cache-strategy.service'; import { CacheController } from './cache.controller'; +import { CacheWarmingService } from './cache-warming.service'; +import { ScheduleModule } from '@nestjs/schedule'; @Module({ imports: [ + ScheduleModule.forRoot(), NestCacheModule.registerAsync({ inject: [ConfigService], useFactory: async (configService: ConfigService) => { @@ -27,8 +30,8 @@ import { CacheController } from './cache.controller'; }, }), ], - providers: [CacheStrategyService], + providers: [CacheStrategyService, CacheWarmingService], controllers: [CacheController], - exports: [CacheStrategyService, NestCacheModule], + exports: [CacheStrategyService, NestCacheModule, CacheWarmingService], }) export class CacheModule {}