Files
sell/middlelayer/utils/redisCache.ts

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;
}
}
}