import Redis from "ioredis"; import { cacheHits, cacheMisses } from "../monitoring/metrics.js"; import { logCacheHit, logCacheMiss } from "../monitoring/logger.js"; import { logger } from "../monitoring/logger.js"; /** * Redis-basierter Cache mit Fallback auf In-Memory */ export class RedisCache { private redis: Redis | null = null; private memoryCache: Map = new Map(); private defaultTTL: number; private cacheType: string; private useRedis: boolean; constructor(defaultTTL: number, cacheType: string) { this.defaultTTL = defaultTTL; this.cacheType = cacheType; this.useRedis = process.env["REDIS_ENABLED"] === "true"; if (this.useRedis) { this.initRedis(); } else { logger.info( `Redis Cache deaktiviert für ${cacheType}, verwende In-Memory Cache` ); } } private async initRedis() { try { this.redis = new Redis({ host: process.env["REDIS_HOST"] || "localhost", port: parseInt(process.env["REDIS_PORT"] || "6379"), password: process.env["REDIS_PASSWORD"], retryStrategy: (times) => { // Bei Fehler: Fallback auf In-Memory nach 3 Versuchen if (times > 3) { logger.warn( "Redis-Verbindung fehlgeschlagen, verwende In-Memory Cache" ); this.useRedis = false; return null; // Stoppe Retry } return Math.min(times * 50, 2000); }, maxRetriesPerRequest: 3, }); this.redis.on("error", (error) => { logger.error("Redis-Fehler", { error, cacheType: this.cacheType }); // Fallback auf In-Memory bei Fehler this.useRedis = false; }); this.redis.on("connect", () => { logger.info(`Redis verbunden für ${this.cacheType}`); }); // Test-Verbindung await this.redis.ping(); logger.info(`Redis Cache aktiviert für ${this.cacheType}`); } catch (error) { logger.warn( "Redis-Initialisierung fehlgeschlagen, verwende In-Memory Cache", { error, cacheType: this.cacheType, } ); this.useRedis = false; } } async set(key: string, data: T, ttl?: number): Promise { const ttlMs = ttl || this.defaultTTL; const serialized = JSON.stringify(data); if (this.useRedis && this.redis) { try { // Redis TTL in Sekunden const ttlSeconds = Math.ceil(ttlMs / 1000); await this.redis.setex(key, ttlSeconds, serialized); return; } catch (error) { logger.warn("Redis set fehlgeschlagen, verwende In-Memory", { error, key, }); this.useRedis = false; } } // Fallback: In-Memory const expiresAt = Date.now() + ttlMs; this.memoryCache.set(key, { data, expiresAt }); } async get(key: string): Promise { if (this.useRedis && this.redis) { try { const value = await this.redis.get(key); if (value) { // Cache Hit cacheHits.inc({ cache_type: this.cacheType }); logCacheHit(key, this.cacheType); return JSON.parse(value) as T; } // Cache Miss cacheMisses.inc({ cache_type: this.cacheType }); logCacheMiss(key, this.cacheType); return null; } catch (error) { logger.warn("Redis get fehlgeschlagen, verwende In-Memory", { error, key, }); this.useRedis = false; } } // Fallback: In-Memory const entry = this.memoryCache.get(key); if (!entry) { cacheMisses.inc({ cache_type: this.cacheType }); logCacheMiss(key, this.cacheType); return null; } if (Date.now() > entry.expiresAt) { this.memoryCache.delete(key); cacheMisses.inc({ cache_type: this.cacheType }); logCacheMiss(key, this.cacheType); return null; } cacheHits.inc({ cache_type: this.cacheType }); logCacheHit(key, this.cacheType); return entry.data; } async clear(): Promise { if (this.useRedis && this.redis) { try { // Lösche alle Keys mit unserem Prefix const keys = await this.redis.keys(`${this.cacheType}:*`); if (keys.length > 0) { await this.redis.del(...keys); } } catch (error) { logger.warn("Redis clear fehlgeschlagen", { error }); } } this.memoryCache.clear(); } async delete(key: string): Promise { if (this.useRedis && this.redis) { try { await this.redis.del(key); return; } catch (error) { logger.warn("Redis delete fehlgeschlagen", { error, key }); } } this.memoryCache.delete(key); } async disconnect(): Promise { if (this.redis) { await this.redis.quit(); this.redis = null; } } }