stock-bot/libs/cache/src/providers/hybrid-cache.ts

261 lines
7.3 KiB
TypeScript

import { createLogger } from '@stock-bot/logger';
import { CacheProvider, CacheOptions, CacheStats } from '../types';
import { RedisCache } from './redis-cache';
import { MemoryCache } from './memory-cache';
/**
* Hybrid cache provider that uses memory as L1 cache and Redis as L2 cache
* Provides the best of both worlds: fast memory access and persistent Redis storage
*/
export class HybridCache implements CacheProvider {
private memoryCache: MemoryCache;
private redisCache: RedisCache;
private logger = createLogger('hybrid-cache');
private enableMetrics: boolean;
private startTime = Date.now();
private stats: CacheStats = {
hits: 0,
misses: 0,
errors: 0,
hitRate: 0,
total: 0,
uptime: 0
};
constructor(options: CacheOptions = {}) {
this.enableMetrics = options.enableMetrics ?? true;
// Create L1 (memory) cache with shorter TTL
this.memoryCache = new MemoryCache({
...options,
ttl: options.memoryTTL ?? 300, // 5 minutes for memory
maxMemoryItems: options.maxMemoryItems ?? 1000,
enableMetrics: false // We'll handle metrics at hybrid level
});
// Create L2 (Redis) cache with longer TTL
this.redisCache = new RedisCache({
...options,
enableMetrics: false // We'll handle metrics at hybrid level
});
this.logger.info('Hybrid cache initialized', {
memoryTTL: options.memoryTTL ?? 300,
redisTTL: options.ttl ?? 3600,
maxMemoryItems: options.maxMemoryItems ?? 1000
});
}
private updateStats(hit: boolean, error = false): void {
if (!this.enableMetrics) return;
if (error) {
this.stats.errors++;
} else if (hit) {
this.stats.hits++;
} else {
this.stats.misses++;
}
this.stats.total = this.stats.hits + this.stats.misses;
this.stats.hitRate = this.stats.total > 0 ? this.stats.hits / this.stats.total : 0;
this.stats.uptime = Date.now() - this.startTime;
}
async get<T>(key: string): Promise<T | null> {
try {
// Try L1 cache first (memory)
const memoryValue = await this.memoryCache.get<T>(key);
if (memoryValue !== null) {
this.updateStats(true);
this.logger.debug('L1 cache hit', { key, hitRate: this.stats.hitRate });
return memoryValue;
}
// Try L2 cache (Redis)
const redisValue = await this.redisCache.get<T>(key);
if (redisValue !== null) {
// Populate L1 cache for next access
await this.memoryCache.set(key, redisValue);
this.updateStats(true);
this.logger.debug('L2 cache hit, populating L1', { key, hitRate: this.stats.hitRate });
return redisValue;
}
// Complete miss
this.updateStats(false);
this.logger.debug('Cache miss (both L1 and L2)', { key });
return null;
} catch (error) {
this.updateStats(false, true);
this.logger.error('Hybrid cache get error', {
key,
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
try {
// Set in both caches
const memoryPromise = this.memoryCache.set(key, value, Math.min(ttl ?? 300, 300));
const redisPromise = this.redisCache.set(key, value, ttl);
await Promise.allSettled([memoryPromise, redisPromise]);
this.logger.debug('Cache set (both L1 and L2)', { key, ttl });
} catch (error) {
this.updateStats(false, true);
this.logger.error('Hybrid cache set error', {
key,
error: error instanceof Error ? error.message : String(error)
});
}
}
async del(key: string): Promise<void> {
try {
// Delete from both caches
const memoryPromise = this.memoryCache.del(key);
const redisPromise = this.redisCache.del(key);
await Promise.allSettled([memoryPromise, redisPromise]);
this.logger.debug('Cache delete (both L1 and L2)', { key });
} catch (error) {
this.updateStats(false, true);
this.logger.error('Hybrid cache delete error', {
key,
error: error instanceof Error ? error.message : String(error)
});
}
}
async exists(key: string): Promise<boolean> {
try {
// Check memory first, then Redis
const memoryExists = await this.memoryCache.exists(key);
if (memoryExists) return true;
return await this.redisCache.exists(key);
} catch (error) {
this.updateStats(false, true);
this.logger.error('Hybrid cache exists error', {
key,
error: error instanceof Error ? error.message : String(error)
});
return false;
}
}
async clear(): Promise<void> {
try {
// Clear both caches
const memoryPromise = this.memoryCache.clear();
const redisPromise = this.redisCache.clear();
await Promise.allSettled([memoryPromise, redisPromise]);
this.logger.info('Cache cleared (both L1 and L2)');
} catch (error) {
this.updateStats(false, true);
this.logger.error('Hybrid cache clear error', {
error: error instanceof Error ? error.message : String(error)
});
}
}
async health(): Promise<boolean> {
try {
const memoryHealthy = await this.memoryCache.health();
const redisHealthy = await this.redisCache.health();
// Hybrid cache is healthy if at least one cache is working
const isHealthy = memoryHealthy || redisHealthy;
this.logger.debug('Hybrid cache health check', {
memory: memoryHealthy,
redis: redisHealthy,
overall: isHealthy
});
return isHealthy;
} catch (error) {
this.logger.error('Hybrid cache health check failed', { error });
return false;
}
}
getStats(): CacheStats {
return {
...this.stats,
uptime: Date.now() - this.startTime
};
}
/**
* Get detailed stats for both cache layers
*/
getDetailedStats() {
return {
hybrid: this.getStats(),
memory: this.memoryCache.getStats(),
redis: this.redisCache.getStats()
};
}
/**
* Warm up the memory cache with frequently accessed keys from Redis
*/
async warmupMemoryCache(keys: string[]): Promise<void> {
this.logger.info('Starting memory cache warmup', { keyCount: keys.length });
let warmed = 0;
for (const key of keys) {
try {
const value = await this.redisCache.get(key);
if (value !== null) {
await this.memoryCache.set(key, value);
warmed++;
}
} catch (error) {
this.logger.warn('Failed to warm up key', { key, error });
}
}
this.logger.info('Memory cache warmup completed', {
requested: keys.length,
warmed
});
}
/**
* Sync memory cache with Redis for specific keys
*/
async syncCaches(keys: string[]): Promise<void> {
for (const key of keys) {
try {
const redisValue = await this.redisCache.get(key);
if (redisValue !== null) {
await this.memoryCache.set(key, redisValue);
} else {
await this.memoryCache.del(key);
}
} catch (error) {
this.logger.warn('Failed to sync key', { key, error });
}
}
}
/**
* Close connections for both caches
*/
async disconnect(): Promise<void> {
await this.redisCache.disconnect();
this.logger.info('Hybrid cache disconnected');
}
}