176 lines
4.8 KiB
TypeScript
176 lines
4.8 KiB
TypeScript
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<T> {
|
|
private redis: Redis | null = null;
|
|
private memoryCache: Map<string, { data: T; expiresAt: number }> = 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<void> {
|
|
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<T | null> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
if (this.redis) {
|
|
await this.redis.quit();
|
|
this.redis = null;
|
|
}
|
|
}
|
|
}
|